diff --git a/dummy b/dummy new file mode 100644 index 00000000..99c53d0e --- /dev/null +++ b/dummy @@ -0,0 +1,44 @@ +dummy +512 512 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 1.0 1.0 +1.0 0.0 0.0 0.0 +0.0 1.0 0.0 0.0 +0.0 0.0 1.0 0.0 +float64 diff --git a/openpiv/calibration/__init__.py b/openpiv/calibration/__init__.py new file mode 100644 index 00000000..b3abb8e2 --- /dev/null +++ b/openpiv/calibration/__init__.py @@ -0,0 +1,60 @@ +""" +================== +Camera Calibration +================== + +This submodule contains functions and routines to calibrate a camera +system. + +DLT Model +========= + camera - Create an instance of a DLT camera model + calibrate_dlt - Calibrate and return DLT coefficients and fitting error + line_intersect - Using two lines, locate where those lines intersect + multi_line_intersect - Using multiple lines, approximate their intersection + +Pinhole Model +============= + calibrate_intrinsics - Calculate the intrinsic parameters using Zang's algorithm + camera - Create an instance of a Pinhole camera model + calibrate_dlt - Calibrate and return DLT coefficients and fitting error + line_intersect - Using two lines, locate where those lines intersect + multi_line_intersect - Using multiple lines, approximate their intersection + +Polynomial Model +================ + camera - Create an instance of a Soloff camera model + multi_line_intersect - Using multiple lines, approximate their intersection + +Marker Detection (Marker Grid) +============================== + detect_markers_template - Detect markers via template correlation + detect_markers_blobs - Detect markers via labeling blobs + get_circular_template - Generate a circular template + get_cross_template - Generate a cross template + preprocess_image - Preprocess calibration image + +Calibration Grid +================ + get_asymmetric_grid - Create an asymmetric rectangular calibration grid + get_simple_grid - Create a simple rectangular calibration grid + +Match Calibration Points +======================== + find_corners - Locate 4 or 6 corners of a calibration grid + find_nearest_points - Find the closest point to a cursor + get_pairs_dlt - Match marker pairs using 4 corners and the DLT algorithm + get_pairs_proj - Match marker pairs using a rough projection estimate + reorder_image_points - Reorder image points in ascending order + show_calibration_image - Plot calibration image and markers + +Utils +===== + homogenize - Homogenize array by appending ones to the last axis + get_image_mapping - Calculate the mappings to rectify a 2D image + get_los_error - Calculate the RMS error of a line of sight (LOS) at a z plane + get_reprojection_error - Calculate the root mean square (RMS) error + get_rmse - Calculate the root mean square error of the residuals + plot_epipolar_line - Plot a 3D representation of epipolar lines + +""" \ No newline at end of file diff --git a/openpiv/calibration/_cal_doc_utils.py b/openpiv/calibration/_cal_doc_utils.py new file mode 100644 index 00000000..1f9edb49 --- /dev/null +++ b/openpiv/calibration/_cal_doc_utils.py @@ -0,0 +1,69 @@ +from scipy._lib import doccer + + +__all__ = ['docfiller'] + + +# typing the same doc string multiple times gets annoying and error prone... +_obj_coords = ( +"""object_points : 2D np.ndarray + Real world coordinates stored in a ndarray structured like [X, Y, Z]'.""") + +_img_coords = ( +"""image_points : 2D np.ndarray + Image coordinates stored in a ndarray structured like [x, y].""") + +_cam_struct = ( +"""cam_struct : dict + A dictionary structure of camera parameters.""") + +_project_points_func = ( +"""project_points_func : function + Projection function with the following signature: + res = func(cam_struct, object_points).""") + +_project_to_z_func = ( +"""project_to_z_func : function + Projection function with the following signiture: + res = func(cam_struct, image_points, Z).""") + +_x_lab_coord = ( +"""X : 1D np.ndarray + Projected world x-coordinates.""") + +_y_lab_coord = ( +"""Y : 1D np.ndarray + Projected world y-coordinates.""") + +_z_lab_coord = ( +"""Z : 1D np.ndarray + Projected world z-coordinates.""") + +_x_img_coord = ( +"""x : 1D np.ndarray + Projected image x-coordinates.""") + +_y_img_coord = ( +"""y : 1D np.ndarray + Projected image y-coordinates.""") + +_project_z = ( +"""z : float + A float specifying the Z (depth) value to project to.""") + +docdict = { + "object_points": _obj_coords, + "image_points": _img_coords, + "cam_struct": _cam_struct, + "project_points_func": _project_points_func, + "project_to_z_func": _project_to_z_func, + "x_lab_coord": _x_lab_coord, + "y_lab_coord": _y_lab_coord, + "z_lab_coord": _z_lab_coord, + "x_img_coord": _x_img_coord, + "y_img_coord": _y_img_coord, + "project_z": _project_z +} + +# SciPy's nifty decorator (works better than my simple implementation) +docfiller = doccer.filldoc(docdict) \ No newline at end of file diff --git a/openpiv/calibration/_calib_utils.py b/openpiv/calibration/_calib_utils.py new file mode 100644 index 00000000..8310086f --- /dev/null +++ b/openpiv/calibration/_calib_utils.py @@ -0,0 +1,393 @@ +import numpy as np +from typing import Tuple +from . import _cal_doc_utils + + +__all__ = [ + "homogenize", + "get_rmse", + "get_reprojection_error", + "get_los_error", + "get_image_mapping" +] + + +def homogenize( + points: np.ndarray +): + """Homogenize points. + + Homogenize points for further processing and correspondence matching. + Points are homogenized as such: + [0, 1, 2, 3, ...] + [0, 1, 2, 3, ...] + [1, 1, 1, 1, ...] <-- Appended ones + + Parameters + ---------- + points : np.ndarray + Points to which ones will be appended to the end of. The array + shape should be [M, N] where M in the number of dimensions and N is + the number of points. + + Returns + ------- + points : np.ndarray + Homogenized points of shape (M+1, N]. + + """ + a1 = np.ones( + (1, points.shape[1]), + dtype=points.dtype + ) + + return np.concatenate([ + points, + a1 + ]) + + +def get_rmse( + error: np.ndarray +): + """Get root mean square error (RMSE). + + Calculate the root mean square error for statistical purposes. + + Parameters + ---------- + error : np.ndarray + The residuals between the predicted value and the actual value. + + Returns + ------- + RMSE : float + The RMSE of the error + + """ + + if len(error.shape) == 2: + square_error = np.sum( + np.square(error), + axis=0 + ) + + elif len(error.shape) == 1: + square_error = np.square(error) + + else: + raise ValueError( + "Residuals (error) array must be of shape (n) or (2, n), " + + f"recieved shape {error.shape}" + ) + + rmse = np.sqrt(np.mean(square_error)) + + return rmse + + +@_cal_doc_utils.docfiller +def get_reprojection_error( + cam: "camera", + object_points: np.ndarray, + image_points: np.ndarray +): + """Calculate camera calibration error. + + Calculate the camera calibration error by projecting object points into image + points and calculating the root mean square (RMS) error. + + Parameters + ---------- + cam : camera + An instance of a camera object. + %(object_points)s + %(image_points)s + + Returns + ------- + RMSE : float + Root mean square (RMS) error of camera parameters. + + Examples + -------- + >>> import numpy as np + >>> from importlib_resources import files + >>> from openpiv.calibration import dlt_model, calib_utils + + >>> path_to_calib = files('openpiv.data').joinpath('test7/D_Cal.csv') + + >>> obj_x, obj_y, obj_z, img_x, img_y = np.loadtxt( + path_to_calib, + unpack=True, + skiprows=1, + usecols=range(5), + delimiter=',' + ) + + >>> obj_points = np.array([obj_x[0:3], obj_y[0:3], obj_z[0:3]], dtype="float64") + >>> img_points = np.array([img_x[0:3], img_y[0:3]], dtype="float64") + + >>> cam = dlt_model.camera( + 'cam1', + [4512, 800] + ) + + >>> cam.minimize_params( + [obj_x, obj_y, obj_z], + [img_x, img_y] + ) + + >>> calib_utils.get_reprojection_error( + cam, + [obj_x, obj_y, obj_z], + [img_x, img_y] + ) + 2.6181007833551034e-07 + + """ + res = cam.project_points( + object_points + ) + + error = res - image_points + + RMSE = get_rmse(error) + + return RMSE + + +@_cal_doc_utils.docfiller +def get_los_error( + cam: "camera", + z: float +): + """Calculate camera LOS error. + + Calculate camera line of sight error at the selected volume depth. + + Parameters + ---------- + cam : camera + An instance of a camera object. + %(project_z)s + + Returns + ------- + RMSE : float + Root mean square (RMS) error of camera parameters. + + Examples + -------- + >>> import numpy as np + >>> from importlib_resources import files + >>> from openpiv.calibration import dlt_model, calib_utils + + >>> path_to_calib = files('openpiv.data').joinpath('test7/D_Cal.csv') + + >>> obj_x, obj_y, obj_z, img_x, img_y = np.loadtxt( + path_to_calib, + unpack=True, + skiprows=1, + usecols=range(5), + delimiter=',' + ) + + >>> obj_points = np.array([obj_x[0:3], obj_y[0:3], obj_z[0:3]], dtype="float64") + >>> img_points = np.array([img_x[0:3], img_y[0:3]], dtype="float64") + + >>> cam = dlt_model.camera( + 'cam1', + [4512, 800] + ) + + >>> cam.minimize_params( + [obj_x, obj_y, obj_z], + [img_x, img_y] + ) + + >>> calib_utils.get_los_error( + cam, + z = 0 + ) + 1.0097171287719555e-12 + + """ + # create a meshgrid for every x and y pixel for back projection. + py, px = np.meshgrid( + np.arange(0, cam.resolution[1]), + np.arange(0, cam.resolution[0]), + indexing="ij" + ) + + image_grid = np.concatenate( + [py.reshape(-1, 1), px.reshape(-1, 1)], + axis=1 + ) + + x = image_grid[:, 1] + y = image_grid[:, 0] + + # get depth + Z = np.zeros_like(x) + z + + # project image coordinates to world points + X, Y, Z = cam.project_to_z( + [x, y], + Z + ) + + # project world points back to image coordinates + res = cam.project_points( + [X, Y, Z] + ) + + error = res - np.array([x, y]) + + RMSE = get_rmse(error) + + return RMSE + + +# This script was originally from Theo's polynomial calibration repository. +@_cal_doc_utils.docfiller +def get_image_mapping( + cam: dict, + project_to_z_func: "function", + project_points_func: "function" +): + """Get image Mapping. + + Get image mapping for rectifying 2D images. + + Parameters + ---------- + cam : camera + An instance of a camera object. + %(project_to_z_func)s + %(project_points_func)s + + Returns + ------- + x : 2D np.ndarray + Mappings for x-coordinates. + y : 2D np.ndarray + Mappings for y-coordinates. + scale : float + Image to world scale factor. + + Examples + -------- + >>> import numpy as np + >>> from importlib_resources import files + >>> from openpiv.calibration import dlt_model, calib_utils + + >>> path_to_calib = files('openpiv.data').joinpath('test7/D_Cal.csv') + + >>> obj_x, obj_y, obj_z, img_x, img_y = np.loadtxt( + path_to_calib, + unpack=True, + skiprows=1, + usecols=range(5), + delimiter=',' + ) + + >>> obj_points = np.array([obj_x[0:3], obj_y[0:3], obj_z[0:3]], dtype="float64") + >>> img_points = np.array([img_x[0:3], img_y[0:3]], dtype="float64") + + >>> cam = dlt_model.camera( + 'cam1', + [4512, 800] + ) + + >>> cam.minimize_params( + [obj_x, obj_y, obj_z], + [img_x, img_y] + ) + + >>> mappings, scale = calib_utils.get_image_mapping( + cam, + ) + + >>> mappings + + >>> scale + + """ + field_shape = ( + cam.resolution[1], + cam.resolution[0] + ) + + # create a meshgrid for every x and y pixel for back projection. + py, px = np.meshgrid( + np.arange(0, cam.resolution[1]), + np.arange(0, cam.resolution[0]), + indexing="ij" + ) + + image_grid = np.concatenate( + [py.reshape(-1, 1), px.reshape(-1, 1)], + axis=-1 + ).astype("float64") + + x = image_grid[:, 1] + y = image_grid[:, 0] + + # We set Z to zero since there is no depth + Z = np.zeros_like(x) + + # project image coordinates to world points + world_x, world_y, _ = cam.project_to_z( + [x, y], + Z + ) + + world_x = world_x.reshape(field_shape, order='C') + world_y = world_y.reshape(field_shape, order='C') + + # get scale + lower_bound_X = np.min(np.absolute(world_x[:, 0])) + upper_bound_X = np.min(np.absolute(world_x[:, -1])) + lower_bound_Y = np.min(np.absolute(world_x[0, :])) + upper_bound_Y = np.min(np.absolute(world_x[-1, :])) + + scale_X = (lower_bound_X + upper_bound_X) / np.size(world_x, 1) + scale_Y = (lower_bound_Y + upper_bound_Y) / np.size(world_x, 0) + + Scale = min(scale_X, scale_Y) + + # get border limits + min_X = np.min(world_x) + max_X = np.max(world_x) + min_Y = np.min(world_y) + max_Y = np.max(world_y) + + # create a meshgrid for every x and y point for forward projection. + X, Y = np.meshgrid( + np.linspace( + min_X + Scale, + max_X, + num=cam.resolution[0], + endpoint=True + ), + np.linspace( + min_Y + Scale, + max_Y, + num=cam.resolution[1], + endpoint=True + ) + ) + + X = np.squeeze(X.reshape(-1, 1)) + Y = np.squeeze(Y.reshape(-1, 1)) + + # project world points to image coordinates + mapped_grid = cam.project_points( + [X, Y, Z] + ) + + mapped_grid_x = mapped_grid[0].reshape(field_shape) + mapped_grid_y = mapped_grid[1].reshape(field_shape) + + return np.array([mapped_grid_x, mapped_grid_y]), Scale \ No newline at end of file diff --git a/openpiv/calibration/_checkerboard_detection.py b/openpiv/calibration/_checkerboard_detection.py new file mode 100644 index 00000000..e69de29b diff --git a/openpiv/calibration/_epipolar_utils.py b/openpiv/calibration/_epipolar_utils.py new file mode 100644 index 00000000..f603526f --- /dev/null +++ b/openpiv/calibration/_epipolar_utils.py @@ -0,0 +1,219 @@ +import numpy as np +from typing import Tuple +from mpl_toolkits import mplot3d +from matplotlib import pyplot as plt + +from . import _cal_doc_utils + +__all__ = [ + "plot_epipolar_line" +] + + +@_cal_doc_utils.docfiller +def plot_epipolar_line( + cam_struct: dict, + project_to_z: "function", + image_points: np.ndarray, + zlims: Tuple[int, int], + ax:"matplotlib.axes.Axes"=None, + color=None +): + """Plot 3D epipolar lines. + + Using the passed camera structure and projection function, plot the 3D + epipolar line(s). By passing ax, multiple epipolar lines can be plotted + as a visualization aid for camera alighnment and multi-camera system + performance. + + Parameters + ---------- + %(cam_struct)s + %(project_to_z_func)s + %(image_points)s + zlims : tuple[int, int] + The start and end values of the epipolar line. + ax : matplotlib.axes.Axes, optional + The axis of which to plot the epipolar line. + color : str, optional + The color of the epipolar line. + + Returns + ------- + fig, ax : matplotlib figure, optional + If an axis is not passed, a new figure and axis will be returned. + + Notes + ----- + This function is based on a similar utilitiy in MyPTV, which is referenced below. + https://github.com/ronshnapp/MyPTV + + """ + Z1, Z2 = zlims + + X1, Y1, Z1 = project_to_z( + cam_struct, + image_points, + z = Z1 + ) + + X2, Y2, Z2 = project_to_z( + cam_struct, + image_points, + z = Z2 + ) + + if ax is None: + fig = plt.figure() + ax = plt.axes(projection='3d') + + if color is None: + ax.plot3D([X1,X2], [Z1,Z2], [Y1,Y2]) + else: + ax.plot3D([X1,X2], [Z1,Z2], [Y1,Y2], c=color) + + return fig, ax + + else: + if color is None: + ax.plot3D([X1,X2], [Z1,Z2], [Y1,Y2]) + else: + ax.plot3D([X1,X2], [Z1,Z2], [Y1,Y2], c=color) + + return None + + +def _line_intersect( + t1: np.ndarray, + r1: np.ndarray, + t2: np.ndarray, + r2: np.ndarray +): + """Calculate where two rays intersect. + + Using two cameras, calculate the world coordinates where two rays + intersect. This is done through an analytical solution based on + direction vectors (r) and camera origins/translations (O). + + Parameters + ---------- + t1: np.ndarray + Three element numpy arrays for camera 1 origin/translation. + r1 : np.ndarray + Three element numpy arrays for camera 1 direction vectors. + t2: np.ndarray + Three element numpy arrays for camera 2 origin/translation. + r2 : np.ndarray + Three element numpy arrays for camera 2 direction vectors. + + Returns + ------- + coords : np.ndarray + The world coordinate that is nearest to the two rays intersecting. + dist : float + The minimum dinstance between the two rays. + + Notes + ----- + Function taken from MyPTV; all rights reserved. The direct link to this + repository is provided below. + https://github.com/ronshnapp/MyPTV + + Examples + -------- + >>> _line_intersect( + t1, r1, + t2, r2 + ) + + """ + r1r2 = r1[0]*r2[0] + r1[1]*r2[1] + r1[2]*r2[2] + r12 = r1[0]**2 + r1[1]**2 + r1[2]**2 + r22 = r2[0]**2 + r2[1]**2 + r2[2]**2 + + dO = t2-t1 + B = [r1[0]*dO[0] + r1[1]*dO[1] + r1[2]*dO[2], + r2[0]*dO[0] + r2[1]*dO[1] + r2[2]*dO[2]] + + # invert matrix to get coefficients a and b + try: + a = (-r22*B[0] + r1r2*B[1])/(r1r2**2 - r12 * r22) + b = (-r1r2*B[0] + r12*B[1])/(r1r2**2 - r12 * r22) + except: + a, b = 0.0, 0.0 + + # now use a and b to calculate the minimum distance + l1 = t1 + a*r1 # line 1 + l2 = t2 + b*r2 # line 2 + dist = sum((l1 - l2)**2)**0.5 # minimum distance + coords = (l1 + l2)*0.5 # coordinate location + + return coords, dist + + +def _multi_line_intersect( + lines: list, + dtype: str="float64" +): + """Calculate where two rays intersect. + + Using two cameras, calculate the world coordinates where two rays + intersect. This is done through an analytical solution based on + direction vectors (r) and camera origins/translations (O). + + Parameters + ---------- + lines : list + A list of lines consisting of a point and direction vectors. + + Returns + ------- + coords : np.ndarray + The world coordinate that is nearest to the intersection of all rays. + + Examples + -------- + >>> _multi_line_intersect( + [ + [_t1, _r1], + [_t2, _r2], + [_t3, _r3] + ] + ) + + """ + # make sure that the direction vector array is 2D + assert(len(lines[0][1].shape) == 2) + + n_cams = len(lines) + n_points = lines[0][1].shape[1] + + A = np.zeros([3,3], dtype=dtype) + b = np.zeros(3, dtype=dtype) + + I = np.eye(3) + + object_points = np.zeros([3, n_points], dtype=dtype) + + # TODO: Optimize this loop as python loops would be slow if we had + # 100,000 particles to iterate + for particle in range(n_points): + A[:, :] = 0.0 + b[:] = 0.0 + + for cam in range(n_cams): + t = lines[cam][0] + r = lines[cam][1][:, particle] + + # This was very important, didn't work without it + r /= np.linalg.norm(r) + + temp = I - np.outer(r, r) + + # Update A matrix and b vector + A += temp + b += temp.dot(t) + + object_points[:, particle] = np.linalg.lstsq(A, b, rcond=None)[0] + + return object_points \ No newline at end of file diff --git a/openpiv/calibration/_marker_detection.py b/openpiv/calibration/_marker_detection.py new file mode 100644 index 00000000..961acd93 --- /dev/null +++ b/openpiv/calibration/_marker_detection.py @@ -0,0 +1,1063 @@ +import numpy as np +from typing import Tuple + +from openpiv.pyprocess import get_field_shape, get_coordinates,\ + sliding_window_array, fft_correlate_images,\ + find_all_first_peaks, find_first_peak + +from skimage.feature import match_template + + +__all__ = [ + "preprocess_image", + "get_circular_template", + "get_cross_template", + "get_new_template", + "detect_markers_template", + "detect_markers_blobs" +] + + +def preprocess_image( + image: np.ndarray, + mask: np.ndarray=None, + roi: list=None, + invert: bool=False, + highpass_sigma: float=None, + lowpass_sigma: float=None, + variance_sigma1: float=None, + variance_sigma2: float=None, + morph_size: int=None, + morph_iter: int=2, + median_size: int=None, + binarize_thresh: float=None +): + """Preprocess calibration image. + + Preprocess calibration image for feature extraction. The final image is a + boolean 2D np.ndarray of shame(n, m). To avoid unwarranted mutation of the + original calibration image, the passed image is explicitly copied. + + Parameters + ---------- + image : 2D np.ndarray + A two dimensional array of pixel intensities of the shape (n, m). + mask : 2D np.ndarray, optional + A 2D boolean np.ndarray where True elements are kept and False elements are + set to zero. + roi : list, optional + A four element list containing min x, y and max x, y in pixels. If a mask + exists, the roi is concatenated with the mask. + invert : bool, optional + If True, invert the image. + highpass_sigma : float. optional + If not None, perform a high pass filter. Pixel intensities below zero are + clipped. + lowpass_sigma : float, optional + If not None, perform a low pass filter on the calibration image. + variance_sigma1, variance_sigma2 : float, optional + If not None, perform a local variance normalization filter. I is best to + start with a sigma around 1 to 5 and increase them until most of the + background is removed. + morph_size : int, optional + If not None, perform erosion and dilation morph_iter amount of times. + This helps remove noise and non-marker elements. + morph_iter : int, optional + The number of iterations to perform the erosion and dilation morphological + operations. + median_size : int, odd, optional + If not None, perform a median filter. + binarize_thresh : float, optional + The threshold used for image binarization. It is a good idea to start with + a low threshold and slowly increase it in order to get an optimal threshold. + + returns + ------- + image : 2D np.ndarray + The enhanced calibration image of shape (n, m). + + """ + from openpiv.preprocess import high_pass, local_variance_normalization + from scipy.ndimage import median_filter, gaussian_filter, grey_dilation, grey_erosion + from scipy.signal import convolve2d + + cal_img = image.copy() + cal_img = cal_img.astype(float, copy=False) + cal_img /= cal_img.max() + + if roi is not None: + if mask is None: + mask = np.ones_like(cal_img, dtype=bool) + + con_mask = np.zeros_like(mask, dtype=mask.dtype) + + con_mask[ + roi[1] : roi[3], # y-axis + roi[0] : roi[2] # x-axis + ] = 1 + + np.multiply( + mask, + con_mask, + out=mask + ) + + if invert == True: + cal_img = 1.0 - cal_img + + if highpass_sigma is not None: + cal_img = high_pass( + cal_img, + sigma=highpass_sigma, + clip=True + ) + + if lowpass_sigma is not None: + cal_img = gaussian_filter( + cal_img, + sigma=lowpass_sigma, + truncate=4 + ) + + if variance_sigma1 is not None and\ + variance_sigma2 is not None: + cal_img = local_variance_normalization( + cal_img, + variance_sigma1, + variance_sigma2, + clip=True + ) + + if morph_size is not None: + if morph_size % 2 != 1: + raise ValueError( + "Morphology size must be odd" + ) + + for _ in range(morph_iter): + cal_img = grey_erosion( + cal_img, + size=morph_size, + mode="constant", + cval=0. + ) + + for _ in range(morph_iter): + cal_img = grey_dilation( + cal_img, + size=morph_size, + mode="constant", + cval=0. + ) + + if median_size is not None: + if median_size % 2 != 1: + raise ValueError( + "Median width must be odd" + ) + + cal_img = median_filter( + cal_img, + size=median_size, + mode="mirror" + ) + + # binarization + cal_img = np.where(cal_img > threshold, 1, 0).astype(bool, copy=False) + + # if a mask is supplied, copy it to minimize unwarranted mutations + if mask is not None: + if roi is not None: + copy = False + else: + copy = True + + mask = mask.astype(bool, copy=copy) + + np.multiply( + cal_img, + mask, + out=cal_img + ) + + return cal_img + + +def get_circular_template( + template_radius, + val=1 +): + """Create a circle template. + + Create a circle template based on the template radius and window size. + This template can be correlated with an image to find features such as + marker detection on calibration plates. + + Parameters + ---------- + template_radius : int + The radius of the circle in the template. + val : float, optional + The value set to the circular elements in the template. + + Returns + ------- + template : 2D np.ndarray + A 2D np.ndarray of dtype np.float64 containing a centralized circular + element. + + Examples + -------- + from openpiv.calib_utils import get_circular_template + + >>> get_circular_template( + template_radius=5, + val=255 + ) + + """ + # make sure input is integer + dot_radius = int(template_radius) + + # get template size + dot_size = 2*template_radius + 1 + + disk = np.zeros( + (dot_size, dot_size), + dtype="float64" + ) + + ys, xs = np.indices([dot_size, dot_size]) + + dist = np.sqrt( + (ys - dot_radius)**2 + + (xs - dot_radius)**2 + ) + + disk[dist <= dot_radius] = val + + return disk + + +def get_cross_template( + template_radius: int, + line_width: int=None, + val: float=1 +): + """Create a cross template. + + Create a cross template based on the template radius and window size. The + line width of the cross is found by int(template_radius / 6) + 1. This + template can be correlated with an image to find features such as marker + detection on calibration plates. + + Parameters + ---------- + template_radius : int + The radius of the cross in the template. + line_width : int, optional + The width of the cross in the template. + val : float, optional + The value set to the cross elements in the template. + + Returns + ------- + template : 2D np.ndarray + A 2D np.ndarray of dtype np.float64 containing a centralized cross + element. + + Examples + -------- + from openpiv.calib_utils import get_cross_template + + >>> get_cross_template( + template_radius=11, + val=255 + ) + + """ + # make sure input is integer + template_radius = int(template_radius) + + # get template size + template_size = 2*template_radius + 1 + + cross = np.zeros( + (template_size, template_size), + dtype="float64" + ) + + ys, xs = np.abs( + np.indices([template_size, template_size]) - template_radius + ) + + if line_width == None: + line_width = int(template_radius / 6) + 1 + + cross[ys < line_width] = val + cross[xs < line_width] = val + + return cross + + +def get_new_template( + image: np.ndarray, + pos_x: np.ndarray, + pos_y: np.ndarray, + template_radius: int, + dtype: str="float64" +): + """Create a new template. + + Create a new template based on image data for further refinement of + marker detection. The template is the mean of all sub-windows in an + image at each selected position. + + Parameters + ---------- + image : np.ndarray + The image which the template will be sampled from. + pos_x, pos_y : np.ndarray + The positions where the sampling occurs. + template_radius : int + The radius of the cross in the template. + dtype : str + The data type of thenew template + + Returns + ------- + template : 2D np.ndarray + A 2D np.ndarray of dtype np.float64 containing a centralized cross + element. + + """ + numel = pos_x.shape[0] + + min_x = template_radius + min_y = template_radius + max_x = image.shape[1] - template_radius - 1 + max_y = image.shape[0] - template_radius - 1 + + template = np.zeros((template_radius*2 + 1, template_radius*2 + 1), dtype=dtype) + denom = 0 + + # Note: this loop can easily be optimized + for i in range(numel): + x, y = int(pos_x[i]), int(pos_y[i]) + + if not ((min_x < x < max_x) and (min_y < y < max_y)): + continue + + lx = x - template_radius + rx = x + template_radius + 1 + ly = y - template_radius + ry = y + template_radius + 1 + + img_cut = image[ly:ry, lx:rx] + + template += img_cut + denom += 1 + + if denom != 0: + template /= denom + + return template + + +def _find_local_max( + corr: np.ndarray, + window_size: int=64, + overlap: int=32, +): + """Find a local maximum from correlation matrix. + + Find all local maximums from the correlation matrix by subdividing the + correlation matrix into multiple subwindows and locating the largest value. + + Parameters + ---------- + corr : np.ndarray + A two dimensional array of marker response values of the shape (n, m). + window_size : int, optional + The size of the window used to search for the local maximum. Must be even + and smaller than the distance between two markers in pixels. A good + rule of thumb is to set the window size to slightly smaller than the + mean marker spacing. + overlap : int, optional + The amount of overlaping pixels for each window. The higher the overlap, + the better local maximums are registered but at the expense of performance + and memory. + + Returns + ------- + max_ind_x, max_ind_y : np.ndarray + A two dimensional array containing local peak indexes of the shape (n, m). + peaks : np.ndarray + A two dimensional array containing local maximums of the shape (n, m). + + """ + # get field shape + field_shape = get_field_shape( + corr.shape, + window_size, + overlap + ) + + # get sub-windows of correlation matrix + corr_windows = sliding_window_array( + corr, + [window_size, window_size], + [overlap, overlap] + ) + + # get location of peaks + max_ind, peaks = find_all_first_peaks( + corr_windows + ) + + max_ind_x = max_ind[:, 2] + max_ind_y = max_ind[:, 1] + + # reshape field (this is not actually needed) + max_ind_x = max_ind_x.reshape(field_shape) + max_ind_y = max_ind_y.reshape(field_shape) + peaks = peaks.reshape(field_shape) + + return max_ind_x, max_ind_y, peaks + + +def _merge_points( + pos_x: np.ndarray, + pos_y: np.ndarray, + merge_radius: int=10, + merge_iter: int=5, + min_count: int=1 +): + """Merge nearby points. + + Merge nearby points iteratively and return positions that have a + minimum amount of merge counts. + + Parameters + ---------- + pos_x, pos_y : np.ndarray + A two dimensional array containing local peak indexes of the shape (n, m). + merge_radius : int, optional + The merge radius when detecting the number of times a marker is found. + Typically, the merge radius should be 5 to 10. + merge_iter : int, optional + The number of iterations to merge neighboring points inside the + merge radius threshold. + min_count : float, optional + The minimum amount a marker is detected. Helps to remove false + positives on marker detection. + + Return + ------ + pos_x, pos_y : np.ndarray + An one dimensional array containing local peak indexes of the shape (n,). + count : np.ndarray + An one dimensional array containing local peak counts of the shape (n,). + + """ + # create 2D array of coordinates + pos = np.array([pos_x, pos_y], dtype="float64").T + + # find clusters + clusters = np.hypot( + pos_x.reshape(-1, 1) - pos_x.reshape(1, -1), + pos_y.reshape(-1, 1) - pos_y.reshape(1, -1) + ) <= merge_radius + + # get mean of clusters iteratively + for _ in range(merge_iter): + for ind in range(pos.shape[0]): + pos[ind, :] = np.mean( + pos[clusters[ind, :], :].reshape(-1, 2), + axis = 0 + ) + + # convert to integers by rounding everything down + new_pos = np.floor(pos) + + # count the number of copies + new_pos, count = np.unique( + new_pos, + return_counts=True, + axis=0 + ) + + # remove positions that are not detected enough times + good_ind = count >= min_count + + new_pos = new_pos[good_ind, :] + count = count[good_ind] + + pos_x = new_pos[:, 0] + pos_y = new_pos[:, 1] + + return pos_x, pos_y, count + + +def _detect_markers( + image: np.ndarray, + template: np.ndarray, + roi: tuple, + window_size: int, + overlap: int, + min_peak_height: float, + merge_radius: float, + merge_iter: int, + min_count: int +): + """Detect marker point candidates. + + Detect marker point candidates by template correlation and thresholding. + The template is correlated globally for faster image processing and uses + a normalized correlation technique using sum of squared differences. + + Parameters + ---------- + image : 2D np.ndarray + A two dimensional array of pixel intensities of the shape (n, m). + template : 2D np.ndarray + A square two dimensional array of the shape (n, m) which is to be correlated + with the image to extract features. Must be of odd shape (e.g., [5, 5]) and + elements must be either 0 or 1. + roi : list, optional + A four element list containing min x, y and max x, y in pixels. + window_size : int, optional + The size of the window used to search for the marker. Must be even + and smaller than the distance between two markers in pixels. A good + rule of thumb is to set the window size to slightly smaller than the + mean marker spacing. + overlap : int, optional + The amount of overlaping pixels for each window. If None, overlap is + automatically set to 75% of the window size. Step size can be + calculated as window_size - overlap. The higher the overlap, the better + markers are registered but at the expense of performance and memory. + min_peak_height : float, optional + Reject correlation peaks below threshold to help remove false positives. + merge_radius : int, optional + The merge radius when detecting the number of times a marker is found. + Typically, the merge radius should be 5 to 10. + merge_iter : int, optional + The number of iterations to merge neighboring points inside the + merge radius threshold. + + Returns + -------. + pos_x, pos_y : np.ndarray + An one dimensional array containing local peak indexes of the shape (n,). + peaks : np.ndarray + An one dimensional array containing local peak heights of the shape (n,). + count : np.ndarray + An one dimensional array containing local peak counts of the shape (n,). + corr : np.ndarray + A two dimensional array containing the tempalte-image correlation field. + + """ + # get roi + off_x = off_y = 0 + + if roi is not None: + off_x = roi[0] + off_y = roi[1] + + corr_slice = ( + slice(roi[1], roi[3]), + slice(roi[0], roi[2]) + ) + + corr = match_template( + image, + template, + pad_input=True + ) + + if roi is not None: + corr_cut = corr[corr_slice] + else: + corr_cut = corr + + corr_padded = np.pad( + corr_cut, + window_size, + mode="constant" + ) + + corr_field_shape = corr_padded.shape + pad_off = window_size + + max_ind_x, max_ind_y, peaks = _find_local_max( + corr_padded, + window_size, + overlap + ) + + # create a grid + grid_x, grid_y = get_coordinates( + corr_field_shape, + window_size, + overlap, + center_on_field=False + ) - np.array([window_size // 2]) + + # add grid to peak indexes to get estimated location + pos_x = grid_x + max_ind_x + pos_y = grid_y + max_ind_y + + # find points near sub window borders and with low peak heights + flags = np.zeros_like(pos_x).astype(bool, copy=False) + p_exclude = 3 + + flags[max_ind_x < p_exclude] = True + flags[max_ind_y < p_exclude] = True + flags[max_ind_x > window_size - p_exclude - 1] = True + flags[max_ind_y > window_size - p_exclude - 1] = True + flags[peaks < min_peak_height] = True + + # remove flagged elements + pos_x = pos_x[~flags] + pos_y = pos_y[~flags] + + # add offsets from roi + pos_x += off_x + pos_y += off_y + + pos_x, pos_y, count = _merge_points( + pos_x, + pos_y, + merge_radius, + merge_iter, + min_count + ) + + # remove padding offsets + pos_x -= pad_off + pos_y -= pad_off + + # find points outside of image + flags = np.zeros_like(pos_x).astype(bool, copy=False) + + n_exclude = 8 + flags[pos_x < n_exclude] = True + flags[pos_y < n_exclude] = True + flags[pos_x > image.shape[1] - n_exclude - 1] = True + flags[pos_y > image.shape[0] - n_exclude - 1] = True + flags[np.isnan(pos_x)] = True + flags[np.isnan(pos_y)] = True + + # remove points outside of image + pos_x = pos_x[~flags] + pos_y = pos_y[~flags] + count = count[~flags] + + return pos_x, pos_y, count, corr + + +def _subpixel_approximation( + corr: np.ndarray, + pos_x: np.ndarray, + pos_y: np.ndarray, + refine_radius: int=3, + refine_iter: int=5, + refine_cutoff: float=0.5 +): + """Iterative subpixel marker refinement. + + Subpixel iterative marker refinement using least squares 2D gaussian + peak fitting. Least squares minimization is performed by taking the + pseudo inverse of a matrix and multiplying it to the logarithm of the + correlation matrix. + + Paramters + --------- + corr : np.ndarray + A two dimensional array of marker response values of the shape (n, m). + pos_x, pos_y : np.ndarray + An one dimensional array containing local peak indexes of the shape (n,). + refine_radius : int, optional + The radius of the gaussian kernel. The radius should be similar to the + radius of the marker radius. However, if the radius is greater than + n_exclude, which is predetermined to be 8, the radius is set to 7. + refine_iter : int, optional + The amount of iterations to perform the gaussian subpixel estimation. + refine_cutoff : float, optional + The cutoff number to stop iterating. Should be between 0.25 to 0.5. + + Returns + ------- + new_pos_x, new_pos_y : np.ndarray + An one dimensional array containing local peak indexes of the shape (n,). + + """ + new_pos_x = pos_x.copy() + new_pos_y = pos_y.copy() + + _sx, _sy = np.meshgrid( + np.arange( + -refine_radius, + refine_radius + 1, + dtype=float + ), + np.arange( + -refine_radius, + refine_radius + 1, + dtype=float + ) + ) + + nx, ny = _sx.shape + + _sx = np.ravel(_sx) + _sy = np.ravel(_sy) + + s_arr = np.array( + [_sx, _sy, _sx**2, _sy**2, np.ones_like(_sx)], + dtype=float + ) + + s_arr = np.reshape( + np.concatenate(s_arr), + (5, nx * ny) + ).T + + # we use a psuedo-inverse via SVD so we can solve a system of equations. + # using iterative nonlinear methods is preferable here, though. + s_inv = np.linalg.pinv(s_arr) + + # TODO: optimize this loop + for ind in range(pos_x.shape[0]): + x, y = pos_x[ind], pos_y[ind] + + for _ in range(refine_iter): + x = int(x) + y = int(y) + + slices = ( + slice( + y - refine_radius, + y + refine_radius + 1), + slice( + x - refine_radius, + x + refine_radius + 1 + ) + ) + + corr_sec = corr[slices] + corr_sec[corr_sec <= 0] = 1e-6 + + coef = np.dot( + s_inv, + np.log(np.ravel(corr_sec)) + ) + + if coef[2] < 0.0 and coef[3] < 0.0: + sx = 1 / np.sqrt(-2 * coef[2]) + sy = 1 / np.sqrt(-2 * coef[3]) + + shift_x = coef[0] * np.square(sx) + shift_y = coef[1] * np.square(sy) + else: + shift_x = 0 + shift_y = 0 + + new_x = x + shift_x + new_y = y + shift_y + + d = np.sqrt((x - new_x)**2 + (y - new_y)**2) + + x = new_x + y = new_y + + if d < refine_cutoff: + break + + new_pos_x[ind] = x + new_pos_y[ind] = y + + return new_pos_x, new_pos_y + + +def detect_markers_template( + image: np.ndarray, + template: np.ndarray, + roi: list=None, + window_size: int=32, + overlap: int=None, + min_peak_height: float=0.25, + merge_radius: int=10, + merge_iter: int=3, + refine_pos: bool=False, + refine_radius: int=3, + refine_iter: int=5, + refine_cutoff: float=0.5, + return_corr: bool=False, + return_template: bool=False +): + """Detect calibration markers. + + Detect circular and cross calibration markers. The markers are first detected + based on correlating the image with a template. Then, the correlation matrix + is split into sub-windows where the position of the markers in the local windows + is found by locating the maximum of that window, which correlates to the best + match with the template. Next, false positives are removed by specifying the + minimum count a marker is detected and the minimum correlation coefficient of + that marker's peak. Finally, a gaussian peak fit based on pseudo-inverse via + singular value decomposition is performed. + + Parameters + ---------- + image : 2D np.ndarray + A two dimensional array of pixel intensities of the shape (n, m). + template : 2D np.ndarray + A square two dimensional array of the shape (n, m) which is to be correlated + with the image to extract features. Must be of odd shape (e.g., [5, 5]) and + elements must be either 0 or 1. + roi : list, optional + A four element list containing min x, y and max x, y in pixels. + window_size : int, optional + The size of the window used to search for the marker. Must be even + and smaller than the distance between two markers in pixels. A good + rule of thumb is to set the window size to slightly smaller than the + mean marker spacing. + overlap : int, optional + The amount of overlapping pixels for each window. If None, overlap is + automatically set to 62.5% of the window size. Step size can be + calculated as window_size - overlap. The higher the overlap, the better + markers are registered but at the expense of performance and memory. + min_peak_height : float, optional + Reject correlation peaks below threshold to help remove false positives. + merge_radius : int, optional + The merge radius when detecting the number of times a marker is found. + Typically, the merge radius should be 5 to 10. + merge_iter : int, optional + The number of iterations to merge neighboring points inside the + merge radius threshold. + refine_pos : bool, optional + Refine the position of the markers with a gaussian peak fit. + refine_radius : int, optional + The radius of the gaussian kernel. The radius should be similar to the + radius of the marker radius. However, if the radius is greater than + n_exclude, which is predetermined to be 8, the radius is set to 7. + refine_temp_radius : int, optional + If set, the new template radius will be 2*refine_temp_radius + 1. + Otherwise, the radius would be 1.25 * template radius. + refine_iter : int, optional + The amount of iterations to perform the gaussian subpixel estimation. + refine_cutoff : float, optional + The cutoff number to stop iterating. Should be between 0.25 to 0.5. + return_corr : bool, optional + Return the correlation of the image and template. This can be used to + determine the template radius. + + Returns + ------- + markers : 2D np.ndarray + Marker positions in [x, y]' image coordinates. + counts : 2D np.ndarray, optional + Marker counts in [x, y]'. Returned if return_count is True. + + Notes + ----- + The gaussian subpixel fitting algorithm is basically a hit or miss when it comes + to improving the marker locations. Because of this, it is not recommended to use + this parameter unless there is a noticable decrease in RMS errors. + + Examples + -------- + Let cal_img be a calibration image. + + >>> template = calib_utils.get_circular_template(radius = 7) + + >>> marks_pos = calib_utils.detect_markers_template( + cal_img, + template, + window_size = 64, + min_peak_height = 0.5, + merge_radius = 10, + merge_iter=2 + ) + + >>> marks_pos + + >>> counts + + """ + # @ErichZimmer + # Note to developers, this function was originally written as a prototype + # for the OpenPIV c++ version. However, it was refined in order to be useful + # for the Python version of OpenPIV. + + min_count = 2 + + # make sure template size is smaller than window size + if template.shape[0] >= window_size: + raise ValueError( + "template_radius is too large for given window_size." + ) + + # if overlap is None, set overlap to 75% of window size + if overlap is None: + overlap = window_size - window_size * 0.325 + + # make sure window_size and overlap are integers + window_size = int(window_size) + overlap = int(overlap) + + # data type conversion to float32 (float64 works too, but is slower) + image = image.astype("float32") + template = template.astype("float32") + + # scale the image to [0, 1] + image[image < 0] = 0. # cut negative pixel intensities + image /= image.max() # normalize +# image *= 255. # rescale + + pos_x, pos_y, counts, corr = _detect_markers( + image, + template, + roi, + window_size, + overlap, + min_peak_height, + merge_radius, + merge_iter, + min_count + ) + + max_radius = 8 + if refine_pos == True: + if refine_radius > max_radius: + refine_radius = max_radius + + pos_x, pos_y = _subpixel_approximation( + corr, + pos_x, + pos_y, + refine_radius, + refine_iter, + refine_cutoff + ) + + return_list = [ + np.array([pos_x, pos_y], dtype="float64") + ] + + if return_corr == True: + return_list.append(corr) + + if return_template == True and refine_template == True: + return_list.append(new_template) + + return return_list + + +def detect_markers_blobs( + image: np.ndarray, + roi: list=None, + min_area: int=None, + max_area: int=None +): + """Detect blob markers. + + Detect blob markers by labeling an image, removing outliers by thresholding, and + finding the center of mass of the labels. + + Parameters + ---------- + image : 2D np.ndarray + A two dimensional array of pixel intensities of the shape (n, m). + roi : list, optional + A four element list containing min x, y and max x, y in pixels. + min_area : int, optional + The minimum amount of pixels a labeled marker can have. + max_area : int, optional + The maximum amount of pixels a labeled marker can have. + + Returns + ------- + markers : 2D np.ndarray + Marker positions in [x, y]' image coordinates. + + Examples + -------- + >>> import numpy as np + >>> from openpiv import calib_utils + >>> from openpiv.data.test5 import cal_image + + >>> cal_img = cal_image(z=0) + + >>> marks_poss = calib_utils.detect_markers_blobs( + cal_img, + roi=[0, 0, None, 950], + min_area=50 + ) + + >>> marks_pos + + """ + from scipy.ndimage import label, center_of_mass + + image = image.astype("float64") + + # set ROI if needed + off_x = off_y = 0 + + if roi is not None: + off_x = roi[0] + off_y = roi[1] + + image = image[ + roi[1] : roi[3], # y-axis + roi[0] : roi[2] # x-axis + ] + + # scale the image to [0, 1] + image[image < 0] = 0. # cut negative pixel intensities + image /= image.max() # normalize + image = image > image.mean() # binarize + + # label possible markers + labels, n_labels = label(image) + + labels_ind = np.arange(1, n_labels + 1) + + # get label area + label_area = np.zeros(n_labels) + for i in labels_ind: + label_area[i-1] = np.sum(labels == i) + + # remove invalid areas + flag = np.zeros(n_labels, dtype=bool) + + if min_area is not None: + flag[label_area < min_area] = True + + if max_area is not None: + flag[label_area > max_area] = True + + valid_labels_ind = labels_ind[~flag] + + # get center of mass of valid labels + _pos = center_of_mass( + image, + labels, + valid_labels_ind + ) + + _pos = np.array(_pos, dtype="float64") + + # rearrange x and y coordinates and apply roi offsets + pos = np.empty_like(_pos, dtype="float64") + + pos[:, 0] = _pos[:, 1] + off_x + pos[:, 1] = _pos[:, 0] + off_y + + # sort so the results behave somewhat like detect_markers_template + order = np.lexsort( + (pos[:, 1], pos[:, 0]) + ) + + return pos[order].T \ No newline at end of file diff --git a/openpiv/calibration/_match_points.py b/openpiv/calibration/_match_points.py new file mode 100644 index 00000000..b683c277 --- /dev/null +++ b/openpiv/calibration/_match_points.py @@ -0,0 +1,529 @@ +import numpy as np +from typing import Tuple +from scipy.spatial import ConvexHull + +from .dlt_model import calibrate_dlt +from ._calib_utils import homogenize +from ._target_grids import get_simple_grid + + +__all__ = [ + "show_calibration_image", + "find_nearest_points", + "get_pairs_dlt", + "get_pairs_proj" +] + + +# @author: Theo +# Created on Thu Mar 25 21:03:47 2021 + +# @ErichZimmer - Changes (June 2, 2023): +# Revised function + +# @ErichZimmer - Changes (Decemmber 2, 2023): +# Revised function +def show_calibration_image( + image: np.ndarray, + markers: np.ndarray, + figsize=(8,11), + radius: int=30, + fontsize=20 +): + """Plot markers on image. + + Plot markers on image and their associated index. This allows one to find the + origin, x-axis, and y-axis point indexes for object-image point matching. + + Parameters + ---------- + image : 2D np.ndarray + A 2D array containing grayscale pixel intensities. + markers : 2D np.ndarray + A 2D array containing image marker coordinates in [x, y]` image coordinates. + radius : int, optional + The radius of the circle drawn around the marker point. + + Returns + ------- + None + + Let cal_img be a calibration image. + + >>> marks_pos = calib_utils.detect_markers_template( + cal_img, + window_size = 64, + template_radius=5, + min_peak_height = 0.2, + merge_radius = 10, + merge_iter=5, + min_count=8, + ) + + >>> calib_utils.show_calibration_image( + cal_img, + marks_pos + ) + + """ + from PIL import Image, ImageFont, ImageDraw + from matplotlib import pyplot as plt + + markers = markers.T + + # funtction to show th clalibration iamge with numbers and circles + plt.close('all') + + marker_numbers=np.arange(0,np.size(markers[:,0])) + + image_p = Image.fromarray(np.uint8((image/np.max(image[::]))*255)) + + draw = ImageDraw.Draw(image_p) + font = ImageFont.truetype("arial.ttf", fontsize) + + for i in range(0, np.size(markers, 0)): + x, y=markers[i,:] + draw.text((x, y), str(marker_numbers[i]), fill=(255), + anchor='mb',font=font) + + plt.figure(1) + fig, ax = plt.subplots(figsize=figsize) + ax.imshow(image_p) + + for marker in markers: + x, y = marker + c = plt.Circle((x, y), radius, color='red', linewidth=2, fill=False) + ax.add_patch(c) + + plt.show() + plt.pause(1) + + +def _reorder_corners( + corners: np.ndarray, + is_square: bool=False +): + """Reorder corners clock-wise and axis-aligned. + + Reorder corner points clock-wise with the x-axis being the largest axis + of the rectangular grid. + + Parameters + ---------- + corners : 2D np.ndarray + A 2D np.ndarray of containing corner points of a rectangular. + is_square : bool, optional + If the target grid is a square, then omit axis alignment. + + Returns + ------- + corners : 2D np.ndarray + A 2D array of containing corner points of a rectangle ordered in a + clock-wise fashion. + + """ + x0 = np.mean(corners[0]) + y0 = np.mean(corners[1]) + + theta = np.arctan2(corners[1] - y0, corners[0] - x0) + + index = np.argsort(theta) + + corners = corners[:, index] + + dist1 = np.linalg.norm(corners[:, 0] - corners[:, 1]) + dist2 = np.linalg.norm(corners[:, 0] - corners[:, -1]) + + if dist2 > dist1 and not is_square: + num_corners = corners.shape[1] + new_index = [(i - 1) % num_corners for i in range(num_corners)] + corners = corners[:, new_index] + + return corners + + +def _get_angle( + point1: np.ndarray, + point2: np.ndarray, + point3: np.ndarray +): + """Calculate an angle between 3 points. + + Calculate an angle between 0 and 360 degrees. This is used for locating + the correct corners of a possibly distorted rectangle. + + Parameters + ---------- + point1 : 2D np.ndarray + A 2D np.ndarray of containing points for the origin of the triangle. + + point1, point2 : 2D np.ndarray + A 2D np.ndarray of containing points for the sides of the triangle. + + Returns + ------- + angle : float + The angle between points 2 and 3 where point 1 is the vertex. + + """ + + angle1 = np.arctan2(point2[1] - point1[1], point2[0] - point1[0]) + angle2 = np.arctan2(point3[1] - point1[1], point3[0] - point1[0]) + + angle = angle1 - angle2 + + pi2 = np.pi * 2 + + if angle < 0: + angle += pi2 + + if angle > np.pi: + angle = pi2 - angle + + return (360 * angle) / pi2 + + +def _find_corners( + image_points: np.ndarray, + asymmetric: bool=False, + is_square=False +): + """Locate corners of a rectangle using a convex hull. + + Locate the corners of a rectangle that may be distorted by obtaining + the perimeter points using a convex hull of the image points and + sorting the perimeter points by its angle between two neighboring + points. For a symmetric grid, the first four points are assumed to be + the correct corner candidates, otherwise the first six points are + selected. + + Parameter + --------- + image_points : 2D np.ndarray + A 2D array of image points in [x, y]' image coordinates. + asymmetric: bool, optional + If true, validate that 6 corners have been found instead of 4. + is_square : bool, optional + If the target grid is a square, then omit axis alignment. + + Returns + ------- + corners : 2D np.ndarray + A 2D array of corner points in [x, y]' image coordinates. + + Raises + ------ + ValueError + if an incorrect amount of corners are detected. + + """ + indexes = ConvexHull(image_points.T).vertices + + candidates = image_points.T[indexes] + + min_ind = 0 + max_ind = candidates.shape[0]-1 + + angles = [] + for i, corner in enumerate(candidates): + + ind1 = i - 1 + ind2 = i + 1 + + if ind1 < 0: + ind1 = max_ind + + if ind2 > max_ind: + ind2 = min_ind + + point1 = candidates[ind1, :] + point2 = candidates[ind2, :] + + angle = _get_angle(corner, point1, point2) + + angles.append(angle) + + indexes = np.argsort(angles) + + num_corners = 4 + + if asymmetric == True: + # num_corners += 2 + raise ValueError( + "Asymmetric grid detection is not currently implemented" + ) + + corners = candidates[indexes[:num_corners], :] + + corners = np.array(corners, dtype="float64").T + + return _reorder_corners(corners, is_square) + + +def find_nearest_points( + image_points: np.ndarray, + point: np.ndarray, + threshold: float=None, + flag_nans: bool=False +): + """Locate the nearest image point. + + Locate the closest image point to the user selected image point. + This function is implemented by find the minimum distance to the + point of interest, so it will always return a point no matter how + (un)realistic that point is. + + Parameters + ---------- + image_points : 2D np.ndarray + A 2D array of image points in [x, y]' image coordinates. + point : 2D np.ndarray + A 2D array of points of interest in [x, y]' image coordinates. + threshold : float, optional + If set, distances that are greater than the threshold are ignored. + flag_nans : bool, optional + If enabled, set flagged points that exceed the threshold to nan. + Otherwise, the flagged points are not returned. + + Returns + ------- + points : 2D np.ndarray + A 2D array of image points in [x, y]' image coordinates. + + """ + dist = np.linalg.norm( + [ + image_points[0][:, np.newaxis] - point[0], + image_points[1][:, np.newaxis] - point[1] + ], + axis=0 + ) + + index = np.argmin(dist, axis = 0) + + set_to_nan = np.zeros_like(index).astype("bool") + + if threshold is not None: + min_dist = np.min(dist, axis=0) + + if flag_nans == True: + set_to_nan[min_dist > threshold] = True + + else: + index = index[min_dist < threshold] + set_to_nan = np.zeros_like(index).astype("bool") + + candidate_points = image_points[:, index] + + candidate_points[:, set_to_nan] = np.nan + + return candidate_points + + +def get_pairs_dlt( + image_points: np.ndarray, + grid_shape: list, + grid: np.ndarray=None, + corners: np.ndarray=None, + asymmetric: bool=False, + sanity_check: bool=True +): + """Match object points to image points via homography. + + Match image points to lab points using the direct linear transformation + correspondence of four corner points. Using the DLT, the correspondences + of the remaining image points can be found and paired with lab points + under the assumption that there is little distortion and the grid is + planar. + + Parameters + ---------- + image_points : 2D np.ndarray + Image coordinates. The ndarray is structured like [x, y]'. + grid_shape : tuple + The shape of the grid for the x and y axis respectively. + grid : 2D np.ndarray, optional + Lab coordinates with an array structed like [x, y]'. If no grid + is supplied, a simple one is automatically created. + corners : 2D np.ndarray, optional + Corners used for point correspondences. If not supplied, corners + are automatically detected using a convex-hull algorithm. + asymmetric : bool + If true, use asymmetric point matching. + sanity_check : bool + If true, check the matched image and object points for duplicates. + + Returns + ------- + img_points : 2D np.ndarray + 2D matched image points of [x, y]' in image coordinates. + obj_points : 2D np.ndarray + 2D matched object points of [x, y, z]' in world coordinates. + + Notes + ----- + Since the direct linear transformation is the similar to the pinhole + camera model without distortion modeling, this algorithm will fail with + the presence of distortion and high point densities. Additionally, + non-planar calibration targets are not compatible with this function. + + """ + if asymmetric: + raise ValueError( + "Asymmetric grid detection is not currently implemented" + ) + + if not isinstance(grid, np.ndarray): + grid = get_simple_grid( + grid_shape[0], grid_shape[1], + 0, 0, 0, + flip_y=False + ) + + grid = grid[:2, :].reshape((2, grid_shape[1], grid_shape[0])) + + real_corners = np.array([ + grid[:, 0, 0], + grid[:, 0, grid_shape[0] - 1], + grid[:, grid_shape[1] - 1, grid_shape[0] - 1], + grid[:, grid_shape[1] - 1, 0] + ]).T + + grid = grid.reshape([2, -1]) + + if not isinstance(corners, np.ndarray): + corners = _find_corners( + image_points, + asymmetric, + is_square=grid_shape[0] == grid_shape[1] + ) + + H, _ = calibrate_dlt( + real_corners, + corners, + enforce_coplanar=True + ) + + rectified = np.dot( + H, + homogenize(grid) + ) + + rectified /= rectified[2, :] + rectified = rectified[:2, :] + + tolerance1 = np.linalg.norm(corners[:, 0] - corners[:, 1]) / grid_shape[0] + tolerance2 = np.linalg.norm(corners[:, 0] - corners[:, -1]) / grid_shape[1] + + tolerance = min(tolerance1, tolerance2) + + reordered_points = find_nearest_points( + image_points, + rectified, + tolerance, + flag_nans=True + ) + + # check for duplicates (only happens with distortion or extraneous markers) + mask = ~np.isnan(reordered_points[0, :]) + + good_points = np.unique( + reordered_points[:, mask], + axis=1 + ) + + if sanity_check == True: + if good_points.shape[1] != reordered_points[:, mask].shape[1]: + raise Exception( + "Failed to sort image points due to multiple points sharing " + + "the same location (this is likely caused by distortion)" + ) + + return reordered_points[:, mask], grid[:, mask] + + +def get_pairs_proj( + cam_struct: dict, + proj_func: "function", + object_points: np.ndarray, + image_points: np.ndarray, + tolerance: float=10 +): + """Match object points to image points via projection. + + Match object points to image points by projection with a rough + calibration of at least 6 points. This method is more reliable than + matching image points to object points based on homographies since + image points and object points are being projected and compared to + each other to find a best match. This allows non-planar calibration + plates to be used. + + Parameters + ---------- + cam_struct : dict + A dictionary structure of camera parameters. + proj_func : function + Projection function with the following signiture: + res = func(cam_struct, object_points). + image_points : 2D np.ndarray + 2D np.ndarray of [x, y]` image coordinates. + object_points : 2D np.ndarray + 2D np.ndarray of [X, Y, Z]` world coordinates. + tolerance : float, optional + The maximum RMS error between the image point and an object point. + + Returns + ------- + img_points : 2D np.ndarray + 2D matched image points of [x, y] in image coordinates. + obj_points : 2D np.ndarray + 2D matched object points of [x, y, z] in world coordinates. + + Notes + ----- + This function is used when a rough calibration is performed over some points of + the calibration plate. These points are usually manually selected and given + world point coordinates. At least 9 points for a pinhole model or 19 points for + a polynomial model are needed since this gives a good enough calibration to pair + the correct object points to image points. + + """ + object_points = np.array(object_points, dtype="float64") + image_points = np.array(image_points, dtype="float64") + + image_points_proj = proj_func( + cam_struct, + object_points + ) + + obj_pairs = [] + img_pairs = [] + + for i in range(image_points.shape[1]): + min_j = -1 + min_rmse = 1000 + + for j in range(image_points_proj.shape[1]): + rmse = np.mean( + np.sqrt( + np.square( + image_points[:, i] - image_points_proj[:, j] + ), + ) + ) + + if rmse < min_rmse: + min_rmse = rmse + min_j = j + + if min_rmse < tolerance: + if min_j == -1: + continue + + obj_pairs.append(object_points[:, min_j]) + img_pairs.append(image_points[:, i]) + + return ( + np.array(img_pairs, dtype="float64").T, + np.array(obj_pairs, dtype="float64").T + ) \ No newline at end of file diff --git a/openpiv/calibration/_target_grids.py b/openpiv/calibration/_target_grids.py new file mode 100644 index 00000000..1b2ee25d --- /dev/null +++ b/openpiv/calibration/_target_grids.py @@ -0,0 +1,172 @@ +import numpy as np +from typing import Tuple + + +__all__ = [ + "get_simple_grid", + "get_asymmetric_grid" +] + + +def get_simple_grid( + nx: int, + ny: int, + z: int, + x_ind: int, + y_ind: int, + flip_y: bool=True, + spacing: float=1.0 +): + """Make a simple rectangular grid. + + Create a simple rectangular grid with the origin based on the index of + the selected point. This allows an arbitrary size grid and location of + origin. + + Parameters + ---------- + nx : int + Number of markers in the x-axis. + ny : int + Number of markers in the y-axis. + z : float, optional + The z plane where the calibration plate is located. + x_ind : int + Index of the point to define the x-axis. + y_ind : int + Index of the point to define the y-axis. + flip_y : bool, optional + Flip the signs of the y-axis. This is use enabled by default, but + if the grid has an origin in the top-left corner, this should be + disabled. + spacing : float, optional + Grid spacing in millimeters. + + Returns + ------- + object_points : 2D np.ndarray + 2D object points of [X, Y, Z] in world coordinates. + + Examples + -------- + >> from openpiv.calib_utils import get_simple_grid + + >>> object_points = get_simple_grid( + nx = 9, + ny = 9, + z = 0, + x_ind = 4, + y_ind = 4 + + >>> object_points + + """ + range_x = np.arange(nx).astype("float64") + range_y = np.arange(ny).astype("float64") + + origin_x = range_x[x_ind] + origin_y = range_y[y_ind] + + range_x -= origin_x + range_y -= origin_y + + range_x *= spacing + range_y *= spacing + + x, y = np.meshgrid(range_x, range_y) + + if flip_y == True: + y = -y + + object_points = np.array( + [x.ravel(), y.ravel(), np.zeros_like(x).ravel() + z], + dtype="float64" + ) + + return object_points + + +def get_asymmetric_grid( + nx: int, + ny: int, + z: int, + x_ind: int, + y_ind: int, + flip_y: bool=True, + spacing: float=1.0 +): + """Make an asymmetrical grid. + + Create an asymmetrical grid with the origin based on the index of the + selected point. This allows an arbitrary size grid and location of + origin. + + Parameters + ---------- + nx : int + Number of markers in the x-axis. + ny : int + Number of markers in the y-axis. + z : float, optional + The z plane where the calibration plate is located. + x_ind : int + Index of the point to define the x-axis. + y_ind : int + Index of the point to define the y-axis. + flip_y : bool, optional + Flip the signs of the y-axis. This is use enabled by default, but + if the grid has an origin in the top-left corner, this should be + disabled. + spacing : float, optional + Grid spacing in millimeters. + + Returns + ------- + object_points : 2D np.ndarray + 2D object points of [X, Y, Z] in world coordinates. + + Examples + -------- + >> from openpiv.calib_utils import get_sasymmetric_grid + + >>> object_points = get_sasymmetric_grid( + nx = 9, + ny = 9, + z = 0, + x_ind = 4, + y_ind = 4 + + >>> object_points + + """ + + simple_grid = get_simple_grid( + nx, + ny, + z, + x_ind, + y_ind, + flip_y, + spacing + ) + + grid = simple_grid.reshape([3, ny, nx]) + + # TODO: optimize this loop + _grid_x = [] + _grid_y = [] + _grid_z = [] + + for i in range(ny): + _grid_x += (grid[0, i, i%2::2].tolist()) + _grid_y += (grid[1, i, i%2::2].tolist()) + _grid_z += (grid[2, i, i%2::2].tolist()) + + return np.array( + [ + _grid_x, + _grid_y, + _grid_z + ], + dtype="float64" + ) \ No newline at end of file diff --git a/openpiv/calibration/calib_utils.py b/openpiv/calibration/calib_utils.py new file mode 100644 index 00000000..5731ef35 --- /dev/null +++ b/openpiv/calibration/calib_utils.py @@ -0,0 +1,8 @@ +from ._calib_utils import * +from ._epipolar_utils import * +from ._marker_detection import * +from ._match_points import * +from ._target_grids import * + + +__all__ = [s for s in dir() if not s.startswith("_")] \ No newline at end of file diff --git a/openpiv/calibration/dlt_model/__init__.py b/openpiv/calibration/dlt_model/__init__.py new file mode 100644 index 00000000..3110720e --- /dev/null +++ b/openpiv/calibration/dlt_model/__init__.py @@ -0,0 +1,32 @@ +""" +========= +DLT Model +========= + +This module contains an implementation of a direct linear transformation +model which is equivalent to a pinhole camera model under conditions with +no distortion. However, this condition is practically impossible to obtain +in laboratory conditions, so calibration errors are typically higher than +that of pinhole and polynomial calibration methods. + +Functions +========= + camera - Create an instance of a DLT camera model + calibrate_dlt - Calibrate and return DLT coefficients and fitting error + line_intersect - Using two lines, locate where those lines intersect + multi_line_intersect - Using multiple lines, approximate their intersection + +Note +==== +It is important to only import the submodule and not the functions that +are in the submodules. Explicitly importing a function from this submodule +could cause conflicts between other camera models due to similar naming +conventions that are normally protected behind namespaces. + +""" +from ._camera import * +from ._epipolar_geom import * +from ._minimization import * + + +__all__ = [s for s in dir() if not s.startswith("_")] \ No newline at end of file diff --git a/openpiv/calibration/dlt_model/_camera.py b/openpiv/calibration/dlt_model/_camera.py new file mode 100644 index 00000000..8a9f1f29 --- /dev/null +++ b/openpiv/calibration/dlt_model/_camera.py @@ -0,0 +1,62 @@ +import numpy as np +from typing import Tuple + +from ._check_params import _check_parameters +from ._minimization import _minimize_params +from ._projection import _project_points, _project_to_z, _get_inverse_vector +from ._utils import _save_parameters, _load_parameters + + +__all__ = ["camera"] + + +class camera(object): + """An instance of a DLT camera model. + + Create an instance of a DLT camera. The DLT camera model is based on + the direct linear transformation where image points and object points + are mapped through a 3x3 or 3x4 matrix. + + Attributes + ---------- + name : str + The name of the camera. + resolution : tuple[int, int] + Resolution of camera in x and y axes respectively. + coeffs : np.ndarray + The coefficients for a DLT matrix. + dtype : str + The dtype used in the projections. + + Methods + ------- + minimize_params + project_points + project_to_z + save_parameters + load_parameters + + """ + def __init__( + self, + name: str, + resolution: Tuple[int, int], + coeffs: np.ndarray=np.identity(3), + dtype: str="float64" + ): + self.name = name + self.resolution = resolution + self.coeffs = coeffs + self.dtype = dtype + + # method definitions + camera._check_parameters = _check_parameters + camera._get_inverse_vector = _get_inverse_vector + camera.minimize_params = _minimize_params + camera.project_points = _project_points + camera.project_to_z = _project_to_z + camera.save_parameters = _save_parameters + camera.load_parameters = _load_parameters + + # check camera params at init + self._check_parameters() \ No newline at end of file diff --git a/openpiv/calibration/dlt_model/_check_params.py b/openpiv/calibration/dlt_model/_check_params.py new file mode 100644 index 00000000..b59d61d4 --- /dev/null +++ b/openpiv/calibration/dlt_model/_check_params.py @@ -0,0 +1,34 @@ +import numpy as np + + +def _check_parameters(self): + if type(self.name) != str: + raise ValueError( + "Camera name must be a string" + ) + + if len(self.resolution) != 2: + raise ValueError( + "Resolution must be a two element tuple" + ) + + if len(self.coeffs.shape) != 2: + raise ValueError( + "DLT coefficients must be 2 dimensional" + ) + + if self.coeffs.shape[0] != 3: + raise ValueError( + "DLT coefficients axis 0 must be of size 3" + ) + + if self.coeffs.shape[1] not in [3, 4]: + raise ValueError( + "DLT coefficients axis 1 must be of size 3 for 2D " + + "and size 4 for 3D transformations" + ) + + if self.dtype not in ["float32", "float64"]: + raise ValueError( + "Dtype is not supported for camera calibration" + ) \ No newline at end of file diff --git a/openpiv/calibration/dlt_model/_epipolar_geom.py b/openpiv/calibration/dlt_model/_epipolar_geom.py new file mode 100644 index 00000000..cdb5fb4e --- /dev/null +++ b/openpiv/calibration/dlt_model/_epipolar_geom.py @@ -0,0 +1,127 @@ +import numpy as np + +from .._epipolar_utils import _line_intersect, _multi_line_intersect + + +__all__ = [ + "line_intersect", + "multi_line_intersect" +] + + +def line_intersect( + cam_1: "dlt_model.camera", + cam_2: "dlt_model.camera", + img_points_1: np.ndarray, + img_points_2: np.ndarray +): + """Calculate where two rays intersect. + + Using two cameras, calculate the world coordinates where two rays + intersect. This is done through an analytical solution based on + direction vectors and camera origins/translations. + + Parameters + ---------- + cam_1, cam_2 : class + An instance of a DLT camera. + img_points_1, img_points_2 : np.ndarray + Image coordinates stored in a ndarray structured like [x, y]'. + + Returns + ------- + coords : np.ndarray + The world coordinate that is nearest to the two rays intersecting. + dist : float + The minimum dinstance between the two rays. + + """ + cam_1._check_parameters() + cam_2._check_parameters() + + dtype1 = cam_1.dtype + dtype2 = cam_2.dtype + + # all cameras should have the same dtype + if dtype1 != dtype2: + raise ValueError( + "Dtypes between camera structures must match" + ) + + img_points_1 = np.array(img_points_1, dtype=dtype1) + img_points_2 = np.array(img_points_2, dtype=dtype2) + + t1, r1 = cam_1._get_inverse_vector( + img_points_1 + ) + + t2, r2 = cam_2._get_inverse_vector( + img_points_2 + ) + + return _line_intersect( + t1[:, np.newaxis], + r1, + t2[:, np.newaxis], + r2 + ) + + +def multi_line_intersect( + cameras: list, + img_points: list, +): + """Calculate where multiple rays intersect. + + Using at least two cameras, calculate the world coordinates where the + rays intersect. This is done through an least squares solution based on + direction vectors and camera origins/translations. + + Parameters + ---------- + cameras : list + A list of instances of DLT cameras. + img_points : list + A list of image coordinates for each canera structure. + + Returns + ------- + coords : np.ndarray + The world coordinate that is nearest to the intersection of all rays. + + """ + n_cams = len(cameras) + n_imgs = len(img_points) + + # make sure each camera has a set of images + if n_cams != n_imgs: + raise ValueError( + f"Camera - image size mismatch. Got {n_cams} cameras and " + + f"{n_imgs} images" + ) + + # check each camera structure + for cam in range(n_cams): + cameras[cam]._check_parameters() + + # all cameras should have the same dtype + dtype1 = cameras[0].dtype + for cam in range(1, n_cams): + dtype2 = cameras[cam].dtype + + if dtype1 != dtype2: + raise ValueError( + "Dtypes between camera structures must match" + ) + + lines = [] + for cam in range(n_cams): + points = np.array(img_points[cam], dtype=dtype1) + + t, r = cameras[cam]._get_inverse_vector( + points + ) + + lines.append([t, r]) + + return _multi_line_intersect(lines, dtype1) \ No newline at end of file diff --git a/openpiv/calibration/dlt_model/_minimization.py b/openpiv/calibration/dlt_model/_minimization.py new file mode 100644 index 00000000..c666b69b --- /dev/null +++ b/openpiv/calibration/dlt_model/_minimization.py @@ -0,0 +1,488 @@ +import numpy as np +from scipy.optimize import curve_fit + +from ._normalization import _standardize_points_2d, _standardize_points_3d +from .._calib_utils import homogenize, get_rmse +from .. import _cal_doc_utils + + +__all__ = [ + "calibrate_dlt" +] + + +@_cal_doc_utils.docfiller +def calibrate_dlt( + object_points: np.ndarray, + image_points: np.ndarray, + enforce_coplanar: bool=False +): + """Coplanar DLT for homography. + + Compute a homography matrix using direct linear transformation. For 2D + lab coordinates, a 2D DLT is performed. For lab coordinates that include + a Z-axis, a 3D DLT is performed. For 3D DLTs, an option to error if the + Z-axis is not co-planar is available. + + Parameters + ---------- + %(object_points)s + %(image_points)s + enforce_coplanar : bool + If a Z plane is supplied in the object points, check whether or not the Z + planes are co-planar. + + Returns + ------- + H : 2D np.ndarray + A 3x3 matrix containing a homography fit for the object and image points. + error : float + The RMSE error of the DLT fit. + + Raises + ------ + ValueError + If the object coordinates contain non-planar z-coordinates and + enforce_coplanar is enabled. + ValueError + If there are not enough points to calculate the DLT coefficients. + + """ + object_points = np.array(object_points, dtype="float64") + image_points = np.array(image_points, dtype="float64") + + ndims = object_points.shape[0] + + min_points = 4 + + if ndims == 3 and enforce_coplanar != True: + min_points += 2 + + if object_points.shape[1] < min_points: + raise ValueError( + f"Too little points to calibrate. Need at least {min_points} points" + ) + + if object_points.shape[1] != image_points.shape[1]: + raise ValueError( + "Object point image point size mismatch" + ) + + if ndims not in [2, 3]: + raise ValueError( + "Object points must be in either [X, Y] (shape = [N, 2]) or [X, Y, Z] "+ + "format (shape = [N, 3]). Recieved shape = [N, {}]".format(ndims) + ) + + if enforce_coplanar == True: + if ndims == 3: + if np.std(object_points[3]) > 0.00001: + raise ValueError( + "Object points must be co-planar" + ) + ndims = 2 + object_points = object_points[:2, :] + + if ndims == 2: + X_raw, Y_raw = object_points + x_raw, y_raw = image_points + + # normalize for better dlt results + [X, Y], obj_norm_mat = _standardize_points_2d(X_raw, Y_raw) + [x, y], img_norm_mat = _standardize_points_2d(x_raw, y_raw) + + # mount constraints + A = np.zeros([x.shape[0] * 2, 9], dtype="float64") + A[0::2, 0] = -X + A[0::2, 1] = -Y + A[0::2, 2] = -1 + A[0::2, 6] = x * X + A[0::2, 7] = x * Y + A[0::2, 8] = x + + A[1::2, 3] = -X + A[1::2, 4] = -Y + A[1::2, 5] = -1 + A[1::2, 6] = y * X + A[1::2, 7] = y * Y + A[1::2, 8] = y + + else: + X_raw, Y_raw, Z_raw = object_points + x_raw, y_raw = image_points + + # normalize for better dlt results + [X, Y, Z], obj_norm_mat = _standardize_points_3d(X_raw, Y_raw, Z_raw) + [x, y], img_norm_mat = _standardize_points_2d(x_raw, y_raw) + + # mount constraints + A = np.zeros([x.shape[0] * 2, 12], dtype="float64") + A[0::2, 0] = -X + A[0::2, 1] = -Y + A[0::2, 2] = -Z + A[0::2, 3] = -1 + A[0::2, 8] = x * X + A[0::2, 9] = x * Y + A[0::2, 10] = x * Z + A[0::2, 11] = x + + A[1::2, 4] = -X + A[1::2, 5] = -Y + A[1::2, 6] = -Z + A[1::2, 7] = -1 + A[1::2, 8] = y * X + A[1::2, 9] = y * Y + A[1::2, 10] = y * Z + A[1::2, 11] = y + + # solve + U, E, V = np.linalg.svd(A, full_matrices=True) + + H = V[-1, :] + H /= H[-1] + H = H.reshape([3, ndims+1]) + + # denormalize DLT matrix + H = np.matmul( + np.matmul( + np.linalg.inv(img_norm_mat), + H + ), + obj_norm_mat + ) + + # compute RMSE error + xy2 = np.dot( + H, + homogenize(object_points) + ) + + res = xy2 / xy2[2, :] + res = res[:2, :] + + error = res - image_points + + RMSE = get_rmse(error) + + return H, RMSE + + +#@_cal_doc_utils.docfiller +#def calibrate_dlt_robust( +# object_points: np.ndarray, +# image_points: np.ndarray, +# enforce_coplanar: bool=False +#): +# """Coplanar DLT for homography. +# +# Compute a homography matrix using direct linear transformation. For 2D +# lab coordinates, a 2D DLT is performed. For lab coordinates that include +# a Z-axis, a 3D DLT is performed. For 3D DLTs, an option to error if the +# Z-axis is not co-planar is available. +# +# Parameters +# ---------- +# %(object_points)s +# %(image_points)s +# enforce_coplanar : bool +# If a Z plane is supplied in the object points, check whether or not the Z +# planes are co-planar. +# +# Returns +# ------- +# H : 2D np.ndarray +# A 3x3 matrix containing a homography fit for the object and image points. +# error : float +# The RMSE error of the DLT fit. +# +# Raises +# ------ +# ValueError +# If the object coordinates contain non-planar z-coordinates and +# enforce_coplanar is enabled. +# ValueError +# If there are not enough points to calculate the DLT coefficients. +# +# """ +# object_points = np.array(object_points, dtype="float64") +# image_points = np.array(image_points, dtype="float64") +# +# ndims = object_points.shape[0] +# +# min_points = 10 +# +# if ndims == 3 and enforce_coplanar != True: +# min_points += 8 +# +# if object_points.shape[1] < min_points: +# raise ValueError( +# f"Too little points to calibrate. Need at least {min_points} points" +# ) +# +# if object_points.shape[1] != image_points.shape[1]: +# raise ValueError( +# "Object point image point size mismatch" +# ) +# +# if ndims not in [2, 3]: +# raise ValueError( +# "Object points must be in either [X, Y] (shape = [2, N]) or [X, Y, Z] "+ +# "format (shape = [3, N]). Recieved shape = [{}, N]".format(ndims) +# ) +# +# if enforce_coplanar == True: +# if ndims == 3: +# if np.std(object_points[3]) > 0.00001: +# raise ValueError( +# "Object points must be co-planar" +# ) +# ndims = 2 +# object_points = object_points[:2, :] +# +# if ndims == 2: +# X_raw, Y_raw = object_points +# x_raw, y_raw = image_points +# +# # normalize for better dlt results +# [X, Y], obj_norm_mat = _standardize_points_2d(X_raw, Y_raw) +# [x, y], img_norm_mat = _standardize_points_2d(x_raw, y_raw) +# +# # mount constraints +# A = np.zeros([x.shape[0] * 2, 21], dtype="float64") +# r2 = X*Z + Y*Y +# r4 = r2 * r2 +# r6 = r4 * r2 +# +# A[0::2, 0] = -X +# A[0::2, 1] = -Y +# A[0::2, 2] = -1 +# +# A[0::2, 3] = -X * r2 +# A[0::2, 4] = -Y * r2 +# A[0::2, 5] = -1 * r2 +# +# A[0::2, 6] = -X * r4 +# A[0::2, 7] = -Y * r4 +# A[0::2, 8] = -1 * r4 +# +# A[0::2, 18] = x * X +# A[0::2, 19] = x * Y +# A[0::2, 20] = x +# +# A[1::2, 9] = -X +# A[1::2, 10] = -Y +# A[1::2, 11] = -1 +# +# A[1::2, 12] = -X * r2 +# A[1::2, 13] = -Y * r2 +# A[1::2, 14] = -1 * r2 +# +# A[1::2, 15] = -X * r4 +# A[1::2, 16] = -Y * r4 +# A[1::2, 17] = -1 * r4 +# +# A[1::2, 18] = y * X +# A[1::2, 19] = y * Y +# A[1::2, 20] = y +# +# else: +# raise ValueError( +# "3D robust DLT is not supported" +# ) +# +# # solve +# U, E, V = np.linalg.svd(A, full_matrices=True) +# +# H = V[-1, :] +# H /= H[-1] +# H = H.reshape([3, 7]) +# +# # denormalize DLT matrix +# H = np.matmul( +# np.matmul( +# np.linalg.inv(img_norm_mat), +# H +# ), +# obj_norm_mat +# ) +# +# # compute RMSE error +# xy2 = np.dot( +# H, +# homogenize(object_points) +# ) +# +# res = xy2 / xy2[2, :] +# res = res[:2, :] +# +# error = res - image_points +# +# RMSE = get_rmse(error) +# +# return H, RMSE + + +@_cal_doc_utils.docfiller +def _minimize_params( + self, + object_points: np.ndarray, + image_points: np.ndarray, + enforce_coplanar: bool=False +): + """Least squares wrapper for DLT calibration. + + A wrapper around the function 'calibrate_dlt' for use with camera structures. + In the future, a robust DLT calibration would be implemented and its + interface would be linked here too. + + Parameters + ---------- + %(object_points)s + %(image_points)s + enforce_coplanar : bool + If a Z plane is supplied in the object points, check whether or not + the Z planes are co-planar. + + Examples + -------- + >>> import numpy as np + >>> from importlib_resources import files + >>> from openpiv.calibration import dlt_model, calib_utils + + >>> path_to_calib = files('openpiv.data').joinpath('test7/D_Cal.csv') + + >>> obj_x, obj_y, obj_z, img_x, img_y = np.loadtxt( + path_to_calib, + unpack=True, + skiprows=1, + usecols=range(5), + delimiter=',' + ) + + >>> obj_points = np.array([obj_x[0:3], obj_y[0:3], obj_z[0:3]], dtype="float64") + >>> img_points = np.array([img_x[0:3], img_y[0:3]], dtype="float64") + + >>> cam = dlt_model.camera( + 'cam1', + [4512, 800] + ) + + >>> cam.minimize_params( + [obj_x, obj_y, obj_z], + [img_x, img_y] + ) + + >>> calib_utils.get_reprojection_error( + cam, + [obj_x, obj_y, obj_z], + [img_x, img_y] + ) + 2.6181007833551034e-07 + + """ + + self._check_parameters() + + dtype = self.dtype + + object_points = np.array(object_points, dtype=dtype) + image_points = np.array(image_points, dtype=dtype) + + H, error = calibrate_dlt( + object_points, + image_points, + enforce_coplanar + ) + + self.coeffs = H + + +def _refine_func2D( + data, + *H +): + h11, h12, h13, h21, h22, h23, h31, h32, h33 = H + + X = data[0::2] + Y = data[1::2] + + x = (h11 * X + h12 * Y + h13) / (h31 * X + h32 * Y + h33) + y = (h21 * X + h22 * Y + h23) / (h31 * X + h32 * Y + h33) + + res = np.zeros_like(data) + + res[0::2] = x + res[1::2] = y + + return res + + +def _refine_jac2D( + data, + *H +): + h11, h12, h13, h21, h22, h23, h31, h32, h33 = H + + X = data[0::2] + Y = data[1::2] + + N = data.shape[0] + + jac = np.zeros((N, 9), dtype="float64") + + s_x = h11 * X + h12 * Y + h13 + s_y = h21 * X + h22 * Y + h23 + w = h31 * X + h32 * Y + h33 + w_sq = w**2 + + jac[0::2, 0] = X / w + jac[0::2, 1] = Y / w + jac[0::2, 2] = 1. / w + jac[0::2, 6] = (-s_x * X) / w_sq + jac[0::2, 7] = (-s_x * Y) / w_sq + jac[0::2, 8] = -s_x / w_sq + + jac[1::2, 3] = X / w + jac[1::2, 4] = Y / w + jac[1::2, 5] = 1. / w + jac[1::2, 6] = (-s_y * X) / w_sq + jac[1::2, 7] = (-s_y * Y) / w_sq + jac[1::2, 8] = -s_y / w_sq + + return jac + + +def _refine2D( + H, + object_points, + image_points +): + + X, Y = object_points + x, y = image_points + + N = X.shape[0] + + init_guess = H.ravel() + + independent = np.zeros(N * 2) + independent[0::2] = X + independent[1::2] = Y + + dependent = np.zeros(N * 2) + dependent[0::2] = x + dependent[1::2] = y + + # curve_fit uses damped Newton-Raphson optimization (aka LM) by default + new_h, _ = curve_fit( + _refine_func2D, + independent, + dependent, + p0=init_guess, + jac=_refine_jac2D + ) + + new_h /= new_h[-1] + new_h = new_h.reshape((3,3)) + + return new_h \ No newline at end of file diff --git a/openpiv/calibration/dlt_model/_normalization.py b/openpiv/calibration/dlt_model/_normalization.py new file mode 100644 index 00000000..16b04233 --- /dev/null +++ b/openpiv/calibration/dlt_model/_normalization.py @@ -0,0 +1,96 @@ +import numpy as np + + +def _standardize_points_2d( + x: np.ndarray, + y: np.ndarray +): + """Standardize x and y coordinates. + + Normalize the x and y coordinates through standardization. This allows + for a better fit when calibrating the direct linear transformation. + + Parameters + ========== + x, y : np.ndarray + The coordinates to normalize through standardization. + + Returns + ======= + x_n, y_n : np.ndarray + Normalized x-y coordinates. + norm_matrix : np.ndarray + The normalization matrix for normalization and denormalization. + + """ + x_a = np.mean(x) + y_a = np.mean(y) + + x_s = np.sqrt(2 / np.std(x)) + y_s = np.sqrt(2 / np.std(y)) + + x_n = x_s * x + (-x_s * x_a) + y_n = y_s * y + (-y_s * y_a) + + xy_norm = np.array([x_n, y_n], dtype="float64") + + norm_mat = np.array( + [ + [x_s, 0, -x_s * x_a], + [0, y_s, -y_s * y_a], + [0, 0, 1] + ], + dtype="float64" + ) + + return xy_norm, norm_mat + + +def _standardize_points_3d( + x: np.ndarray, + y: np.ndarray, + z: np.ndarray +): + """Standardize x, y and z coordinates. + + Normalize the x, y and z coordinates through standardization. This allows + for a better fit when calibrating the direct linear transformation. + + Parameters + ========== + x, y, z : np.ndarray + The coordinates to normalize through standardization. + + Returns + ======= + x_n, y_n, z_n : np.ndarray + Normalized x, y, z coordinates. + norm_matrix : np.ndarray + The normalization matrix for further normalization and denormalization. + + """ + x_a = np.mean(x) + y_a = np.mean(y) + z_a = np.mean(z) + + x_s = np.sqrt(2 / np.std(x)) + y_s = np.sqrt(2 / np.std(y)) + z_s = np.sqrt(2 / np.std(z)) + + x_n = x_s * x + (-x_s * x_a) + y_n = y_s * y + (-y_s * y_a) + z_n = z_s * z + (-z_s * z_a) + + xyz_norm = np.array([x_n, y_n, z_n], dtype="float64") + + norm_mat = np.array( + [ + [x_s, 0, 0, -x_s * x_a], + [0, y_s, 0, -y_s * y_a], + [0, 0, z_s, -z_s * z_a], + [0, 0, 0, 1] + ], + dtype="float64" + ) + + return xyz_norm, norm_mat \ No newline at end of file diff --git a/openpiv/calibration/dlt_model/_projection.py b/openpiv/calibration/dlt_model/_projection.py new file mode 100644 index 00000000..f4b07287 --- /dev/null +++ b/openpiv/calibration/dlt_model/_projection.py @@ -0,0 +1,235 @@ +import numpy as np + +from .._calib_utils import homogenize +from .. import _cal_doc_utils + + +@_cal_doc_utils.docfiller +def _project_points( + self, + object_points: np.ndarray +): + """Project lab coordinates to image points. + + Parameters + ---------- + camera : class + An instance of a DLT camera. + %(object_points)s + + Returns + ------- + %(x_img_coord)s + %(y_img_coord)s + + Examples + -------- + >>> import numpy as np + >>> from importlib_resources import files + >>> from openpiv.calibration import dlt_model + + >>> path_to_calib = files('openpiv.data').joinpath('test7/D_Cal.csv') + + >>> obj_x, obj_y, obj_z, img_x, img_y = np.loadtxt( + path_to_calib, + unpack=True, + skiprows=1, + usecols=range(5), # get first 5 columns of data + delimiter=',' + ) + + >>> obj_points = np.array([obj_x[0:3], obj_y[0:3], obj_z[0:3]], dtype="float64") + >>> img_points = np.array([img_x[0:3], img_y[0:3]], dtype="float64") + + >>> cam = dlt_model.camera( + 'cam1', + [4512, 800] + ) + + >>> cam.minimize_params( + [obj_x, obj_y, obj_z], + [img_x, img_y] + ) + + >>> cam.project_points( + obj_points + ) + array([[-44.33764399, -33.67518588, -22.97467733], + [ 89.61102874, 211.88636408, 334.59805555]]) + + >>> img_points + array([[-44.33764398, -33.67518587, -22.97467733], + [ 89.61102873, 211.8863641 , 334.5980555 ]]) + + """ + self._check_parameters() + + dtype = self.dtype + H = self.coeffs + + object_points = np.array(object_points, dtype=dtype) + + ndim = H.shape[1] - 1 + + if ndim == 2: + object_points = object_points[:2, :] + else: + object_points = object_points[:3, :] + + # compute RMSE error + xy = np.dot( + H, + homogenize(object_points) + ) + + xy /= xy[2, :] + img_points = xy[:2, :] + + return img_points.astype(dtype, copy=False) + + +@_cal_doc_utils.docfiller +def _get_inverse_vector( + self, + image_points: np.ndarray +): + """Get pixel to direction vector. + + Calculate a direction vector from a pixel. This vector can be used for + forward projection along a ray using y + ar where y is the origin of + the camera, a is the z plane along the ray, and r is the direction + vector. + + Parameters + ---------- + %(image_points)s + + Returns + ------- + tx, ty, tz : 1D np.ndarray + Direction vector for each respective axis. + dx, dy, dz : 1D np.ndarray + The camera center for each respective axis. + + Notes + ----- + The direction vector is not normalized. + + """ + dtype = self.dtype + p = self.coeffs + + if p.shape != (3, 4): + raise ValueError( + "DLT coefficients must be of shape (3, 4); recieved shape " + + f"{p.shape}" + ) + + m = p[:, :3] + t_un = p[:, 3] + m_inv = np.linalg.inv(m) + + # direction vector + r = np.dot(m_inv, homogenize(image_points)).astype(dtype, copy=False) + + # camera center/translation + t = np.dot(-m_inv, t_un).astype(dtype, copy=False) + + return t, r + + +@_cal_doc_utils.docfiller +def _project_to_z( + self, + image_points: np.ndarray, + z: np.ndarray +): + """Project image points to world points. + + Project image points to world points at specified z-plane using a + closed form solution (when omiting distortion correction). This means + under ideal circumstances with no distortion, the forward project + coordinates would be accurate down to machine precision or numerical + round off errors. + + Parameters + ---------- + %(image_points)s + %(project_z)s + + Returns + ------- + %(x_lab_coord)s + %(y_lab_coord)s + %(z_lab_coord)s + + Examples + -------- + >>> import numpy as np + >>> from importlib_resources import files + >>> from openpiv.calibration import dlt_model + + >>> path_to_calib = files('openpiv.data').joinpath('test7/D_Cal.csv') + + >>> obj_x, obj_y, obj_z, img_x, img_y = np.loadtxt( + path_to_calib, + unpack=True, + skiprows=1, + usecols=range(5), # get first 5 columns of data + delimiter=',' + ) + + >>> obj_points = np.array([obj_x[0:3], obj_y[0:3], obj_z[0:3]], dtype="float64") + >>> img_points = np.array([img_x[0:3], img_y[0:3]], dtype="float64") + + >>> cam = dlt_model.camera( + 'cam1', + [4512, 800] + ) + + >>> cam.minimize_params( + [obj_x, obj_y, obj_z], + [img_x, img_y] + ) + + >>> ij = cam.project_points( + obj_points + ) + + >>> ij + array([[-44.33764399, -33.67518588, -22.97467733], + [ 89.61102874, 211.88636408, 334.59805555]]) + + >>> cam.project_to_z( + ij, + z=obj_points[2] + ) + array([[-105., -105., -105.], + [ -15., -10., -5.], + [ -10., -10., -10.]]) + + >>> obj_points + array([[-105., -105., -105.], + [ -15., -10., -5.], + [ -10., -10., -10.]]) + + """ + self._check_parameters() + + dtype = self.dtype + + image_points = np.array(image_points, dtype=dtype) + + t, r = self._get_inverse_vector( + image_points + ) + + tx, ty, tz = t + dx, dy, dz = r + + a = (z - tz) / dz + + X = a*dx + tx + Y = a*dy + ty + + return np.array([X, Y, np.zeros_like(X) + z], dtype=dtype) \ No newline at end of file diff --git a/openpiv/calibration/dlt_model/_utils.py b/openpiv/calibration/dlt_model/_utils.py new file mode 100644 index 00000000..054556b8 --- /dev/null +++ b/openpiv/calibration/dlt_model/_utils.py @@ -0,0 +1,96 @@ +import numpy as np +from os.path import join +from typing import Tuple + +from .. import _cal_doc_utils + + +def _save_parameters( + self, + file_path: str, + file_name: str=None +): + """Save DLT camera parameters. + + Save the DLT camera parameters to a text file. + + Parameters + ---------- + file_path : str + File path where the camera parameters are saved. + file_name : str, optional + If specified, override the default file name. + + Returns + ------- + None + + """ + if file_name is None: + file_name = self.name + + full_path = join(file_path, file_name) + + with open(full_path, 'w') as f: + f.write(self.name + '\n') + + _r = '' + for i in range(2): + _r += str(self.resolution[i]) + ' ' + + f.write(_r + '\n') + + for i in range(3): + _c = '' + for j in range(self.coeffs.shape[1]): + _c += str(self.coeffs[i, j]) + ' ' + + f.write(_c + '\n') + + f.write(self.dtype + '\n') + + +@_cal_doc_utils.docfiller +def _load_parameters( + self, + file_path: str, + file_name: str +): + """Load DLT camera parameters. + + Load the DLT camera parameters from a text file. + + Parameters + ---------- + file_path : str + File path where the camera parameters are saved. + file_name : str + Name of the file that contains the camera parameters. + + Returns + ------- + None + + """ + full_path = join(file_path, file_name) + + with open(full_path, 'r') as f: + + name = f.readline()[:-1] + + _r = f.readline()[:-2] + resolution = np.array([float(s) for s in _r.split()]) + + coefficients = [] + for i in range(3): + _c = f.readline()[:-2] + coefficients.append(np.array([float(s) for s in _c.split()])) + + dtype = f.readline()[:-1] + + coefficients = np.array(coefficients, dtype = dtype) + + self.name = name + self.resolution = resolution + self.coeffs = coefficients + self.dtype = dtype \ No newline at end of file diff --git a/openpiv/calibration/pinhole_model/__init__.py b/openpiv/calibration/pinhole_model/__init__.py new file mode 100644 index 00000000..025d18ba --- /dev/null +++ b/openpiv/calibration/pinhole_model/__init__.py @@ -0,0 +1,30 @@ +""" +==================== +Pinhole Camera Model +==================== + +This module contains an implementation of the pinhole camera model. This +model is an approximation of how light rays are captured by a camera. Under +ideal circumstances, lab coordinates can be mapped to image sensor +coordinates (also known as pixel coordinates). However, cameras are not +usually ideal and are placed arbitrarily in the lab space. This means that +the lab coordinates have to be transformed into normalized camera +coordinates to remove this arbitrary translation. The normalized camera +coordinates are calculated as explained in readme pinhole camera model file. + + +Functions +========= + calibrate_intrinsics - Calculate the intrinsic parameters using Zang's algorithm + camera - Create an instance of a Pinhole camera model + calibrate_dlt - Calibrate and return DLT coefficients and fitting error + line_intersect - Using two lines, locate where those lines intersect + multi_line_intersect - Using multiple lines, approximate their intersection + +""" +from ._camera import * +from ._epipolar_geom import * +from ._zang import * + + +__all__ = [s for s in dir() if not s.startswith("_")] \ No newline at end of file diff --git a/openpiv/calibration/pinhole_model/_camera.py b/openpiv/calibration/pinhole_model/_camera.py new file mode 100644 index 00000000..60ae483f --- /dev/null +++ b/openpiv/calibration/pinhole_model/_camera.py @@ -0,0 +1,141 @@ +import numpy as np +from typing import Tuple + +from ._check_params import _check_parameters +from ._distortion import (_undistort_points_brown, _distort_points_brown, + _undistort_points_poly, _distort_points_poly) +from ._minimization import _minimize_params +from ._projection import (_normalize_world_points, _normalize_image_points, + _project_points, _project_to_z, _get_inverse_vector) +from ._utils import _get_rotation_matrix, _save_parameters, _load_parameters + + +__all__ = ["camera"] + + +class camera(object): + """An instance of a DLT camera model. + + Create an instance of a pinhole camera. The pinhole camera model is + based on a physically constrained model where image points and object + points are mapped through an extrinsic and intrinsic matrix with a + distortion compensation model. + + Attributes + ---------- + name : str + Name of camera. + resolution : tuple[int, int] + Resolution of camera in x and y axes respectively. + translation : 1D np.ndarray-like + Location of camera origin/center in x, y, and z axes respectively. + orientation : 1D np.ndarray-like + Orientation of camera in x, y, z axes respectively. + rotation : 2D np.ndarray + Rotational camera parameter for camera system. + distortion_model : str + The type of distortion model to use. + + ``brown`` + The Brown model follows the distortion model incorporated by OpenCV. + It consists of a radial and tangential model to compensate for + distortion. + + ``polynomial`` + The polynomial model is used for general distortion compensation. It + consists of a 2nd order polynomial in the x and y axes. This + distortion model relies on the theory put forth by MyPTV and copies + the model in whole. + + Both models do not attempt to correct distortions along the z-plane. + + distortion1 : 2D np.ndarray + Radial and tangential distortion compensation matrix for a camera. + distortion2 : 2D np.ndarray + 2nd order polynomial distortion compensation matrix for a camera. + focal : tuple[float, float] + Focal distance/magnification of camera-lens system for x any y axis + respectively. + principal : tuple[float, float] + Principal point offset for x any y axis respectively. + dtype : str + The dtype used in the projections. All data is copied if the dtype is + different. It is highly unadvisable to change this parameter. + + Methods + ------- + minimize_params + project_points + project_to_z + save_parameters + load_parameters + + References + ---------- + .. [1] Shnapp, R. (2022). MyPTV: A Python Package for 3D Particle + Tracking. J. Open Source Softw., 7, 4398. + + .. [2] Čuljak, I., Abram, D., Pribanić, T., Džapo, H., & Cifrek, M. + (2012). A brief introduction to OpenCV. 2012 Proceedings of the + 35th International Convention MIPRO, 1725-1730. + + """ + def __init__( + self, + name: str, + resolution: Tuple[int, int], + translation: np.ndarray=[0, 0, 1], + orientation: np.ndarray=np.zeros(3, dtype="float64"), + distortion_model: str="polynomial", + distortion1: np.ndarray=np.zeros(8, dtype="float64"), + distortion2: np.ndarray=np.zeros([2, 5], dtype="float64"), + focal: Tuple[float, float]=[1.0, 1.0], + principal: Tuple[float, float]=None, + dtype: str="float64" + ): + translation = np.array(translation, dtype=dtype) + orientation = np.array(orientation, dtype=dtype) + distortion1 = np.array(distortion1, dtype=dtype) + distortion2 = np.array(distortion2, dtype=dtype) + + self.name = name + self.resolution = resolution + self.translation = np.array(translation) + self.orientation = np.array(orientation) + self.distortion_model = distortion_model + self.distortion1 = distortion1 + self.distortion2 = distortion2 + self.focal = focal + self.dtype = dtype + + if principal is not None: + self.principal = principal + else: + # temporary place holder + self.principal = [0, 0] + + # method definitions + camera._check_parameters = _check_parameters + camera._get_inverse_vector = _get_inverse_vector + camera._get_rotation_matrix = _get_rotation_matrix + camera._normalize_world_points = _normalize_world_points + camera._normalize_image_points = _normalize_image_points + camera._undistort_points_brown = _undistort_points_brown + camera._distort_points_brown = _distort_points_brown + camera._undistort_points_poly = _undistort_points_poly + camera._distort_points_poly = _distort_points_poly + camera.minimize_params = _minimize_params + camera.project_points = _project_points + camera.project_to_z = _project_to_z + camera.save_parameters = _save_parameters + camera.load_parameters = _load_parameters + + # check parameters + self._check_parameters() + + # fix principal point if necessary (placed here due to error checking) + if principal is None: + self.principal = [self.resolution[0] / 2, self.resolution[1] / 2] + + # get roation matrix + self._get_rotation_matrix() \ No newline at end of file diff --git a/openpiv/calibration/pinhole_model/_check_params.py b/openpiv/calibration/pinhole_model/_check_params.py new file mode 100644 index 00000000..76a19697 --- /dev/null +++ b/openpiv/calibration/pinhole_model/_check_params.py @@ -0,0 +1,72 @@ +import numpy as np + + +def _check_parameters(self): + """Check camera parameters""" + + if type(self.name) != str: + raise ValueError( + "Camera name must be a string" + ) + + if len(self.resolution) != 2: + raise ValueError( + "Resolution must be a two element tuple" + ) + + if self.translation.shape != (3,): + raise ValueError( + "Translation must be a three element 1D numpy ndarray" + ) + + if self.orientation.shape != (3,): + raise ValueError( + "Orientation must be a three element 1D numpy ndarray" + ) + + if self.distortion_model.lower() not in ["brown", "polynomial"]: + raise ValueError( + "Distortion model must be either 'brown' or 'polynomial', not " + + f"'{self.distortion_model}'." + ) + + if self.distortion1.shape != (8,): + raise ValueError( + "Radial and tangential distortion coefficients must be " +\ + "an 8 element 1D numpy ndarray" + ) + + if not isinstance(self.distortion2, np.ndarray): + raise ValueError( + "Polynomial distortion coefficients must be a numpy ndarray" + ) + + if self.distortion2.shape != (2, 5): + raise ValueError( + "Polynomial distortion coefficients must be a 2x5 numpy ndarray" + ) + + if not isinstance(self.focal, (tuple, list, np.ndarray)): + raise ValueError( + "Focal point must be a tuple or list" + ) + + if len(self.focal) != 2: + raise ValueError( + "Focal point must be a two element tuple or list" + ) + + if not isinstance(self.principal, (tuple, list, np.ndarray)): + raise ValueError( + "Principal point must be a tuple or list" + ) + + if len(self.principal) != 2: + raise ValueError( + "Principal point must be a two element tuple or list" + ) + + if self.dtype not in ["float32", "float64"]: + raise ValueError( + "Dtype is not supported for camera calibration" + ) \ No newline at end of file diff --git a/openpiv/calibration/pinhole_model/_distortion.py b/openpiv/calibration/pinhole_model/_distortion.py new file mode 100644 index 00000000..25acb504 --- /dev/null +++ b/openpiv/calibration/pinhole_model/_distortion.py @@ -0,0 +1,233 @@ +import numpy as np + + +def _undistort_points_brown( + self, + xd: np.ndarray, + yd: np.ndarray +): + """Undistort normalized points. + + Undistort normalized camera points using a radial and tangential + distortion model. + + Parameters + ---------- + xd : 1D np.ndarray + Distorted x-coordinates. + yd : 1D np.ndarray + Distorted y-coordinates. + + Returns + ------- + x : 1D np.ndarray + Undistorted x-coordinates. + y : 1D np.ndarray + Undistorted y-coordinates. + + Notes + ----- + Distortion model is based off of OpenCV. The direct link where the + distortion model was accessed is provided below. + https://docs.opencv.org/3.4/d9/d0c/group__calib3d.html + + """ + k = self.distortion1 + dtype = self.dtype + + r2 = xd*xd + yd*yd + r4 = r2 * r2 + r6 = r4 * r2 + + num = 1 + k[0]*r2 + k[1]*r4 + k[4]*r6 + den = 1 + k[5]*r2 + k[6]*r4 + k[7]*r6 + + delta_x = k[2]*2*xd*yd + k[3]*(r2 + 2*xd*xd) + delta_y = k[3]*2*xd*yd + k[2]*(r2 + 2*yd*yd) + + x = xd*(num / den) + delta_x + y = yd*(num / den) + delta_y + + return np.array([x, y], dtype=dtype) + + +def _distort_points_brown( + self, + x: np.ndarray, + y: np.ndarray +): + """Distort normalized points. + + Distort normalized camera points using a radial and tangential + distortion model. + + Parameters + ---------- + x : 1D np.ndarray + Undistorted x-coordinates. + y : 1D np.ndarray + Undistorted y-coordinates. + + Returns + ------- + xd : 1D np.ndarray + Distorted x-coordinates. + yd : 1D np.ndarray + Distorted y-coordinates. + + Notes + ----- + Distortion model is based off of OpenCV. The direct link where the + distortion model was accessed is provided below. + https://docs.opencv.org/3.4/d9/d0c/group__calib3d.html + + """ + k = self.distortion1 + dtype = self.dtype + + r2 = x*x + y*y + r4 = r2 * r2 + r6 = r4 * r2 + + den = 1 + k[0]*r2 + k[1]*r4 + k[4]*r6 + num = 1 + k[5]*r2 + k[6]*r4 + k[7]*r6 + + delta_x = k[2]*2*x*y + k[3]*(r2 + 2*x*x) + delta_y = k[3]*2*x*y + k[2]*(r2 + 2*y*y) + + xd = (x - delta_x) * (num / den) + yd = (y - delta_y) * (num / den) + + return np.array([xd, yd], dtype=dtype) + + +def _undistort_points_poly( + self, + xd: np.ndarray, + yd: np.ndarray +): + """Undistort normalized points. + + Undistort normalized camera points using a polynomial distortion model. + + Parameters + ---------- + xd : 1D np.ndarray + Distorted x-coordinates. + yd : 1D np.ndarray + Distorted y-coordinates. + + Returns + ------- + x : 1D np.ndarray + Undistorted x-coordinates. + y : 1D np.ndarray + Undistorted y-coordinates. + + Notes + ----- + Distortion model based wholly on by MyPTV. The link is provided below. + https://github.com/ronshnapp/MyPTV/tree/master/myptv + + The polynomial is of the 2nd order type, with the coefficients arranged + like such: coeff = [1, x, y, x**2, y**2, x*y]. This effectively allows + any distortion in the x and y axes to be compensated. However, the + polynomial model is not stable when extrapolating, so beware of + artifcacts. + + References + ---------- + .. [1] Shnapp, R. (2022). MyPTV: A Python Package for 3D Particle + Tracking. J. Open Source Softw., 7, 4398. + + """ + dtype = self.dtype + coeffs = self.distortion2 + + poly = np.array([xd, yd, xd**2, yd**2, xd * yd]) + + e_ = np.dot(coeffs, poly) + + # Calculate derivatives of the polynomials + e_0 = e_[0] + a, b, c, d, ee = coeffs[0,:] + e_xd_0 = a + 2*c*xd + ee*yd + e_yd_0 = b + 2*d*yd + ee*xd + + e_1 = e_[1] + a, b, c, d, ee = coeffs[1,:] + e_xd_1 = a + 2*c*xd + ee*yd + e_yd_1 = b + 2*d*yd + ee*xd + + # Calculate the inverse of the polynomials using derivatives + A11 = 1.0 + e_xd_0 + A12 = e_yd_0 + A21 = e_xd_1 + A22 = 1.0 + e_yd_1 + + rhs1 = xd*(1.0 + e_xd_0) + yd*e_yd_0 - e_0 + rhs2 = yd*(1.0 + e_yd_1) + xd*e_xd_1 - e_1 + + Ainv = np.array([[A22, -A12],[-A21, A11]]) / (A11*A22 - A12*A21) + + xn = Ainv[0, 0] * rhs1 + Ainv[0, 1] * rhs2 + yn = Ainv[1, 0] * rhs1 + Ainv[1, 1] * rhs2 + + return np.array([xn, yn], dtype=dtype) + + +def _distort_points_poly( + self, + xn: np.ndarray, + yn: np.ndarray +): + """Distort normalized points. + + Distort normalized camera points using a polynomial distortion model. + + Parameters + ---------- + xn : 1D np.ndarray + Undistorted x-coordinates. + yn : 1D np.ndarray + Undistorted y-coordinates. + + Returns + ------- + xd : 1D np.ndarray + Distorted x-coordinates. + yd : 1D np.ndarray + Distorted y-coordinates. + + Notes + ----- + Distortion model is inspired by MyPTV. The link is provided below. + https://github.com/ronshnapp/MyPTV/tree/master/myptv + + The polynomial is of the 2nd order type, with the coefficients arranged + like such: coeff = [x, y, x**2, y**2, x*y]. This effectively allows + any distortion in the x and y axes to be compensated. However, the + polynomial model is not stable when extrapolating, so beware of + artifcacts. + + To compute the inverse of the distortion model, we use compute the error + term and add it to the normalized camera coordinated. This method does + not require further iterations for refiunement. + + References + ---------- + .. [1] Shnapp, R. (2022). MyPTV: A Python Package for 3D Particle + Tracking. J. Open Source Softw., 7, 4398. + + """ + dtype = self.dtype + coeffs = self.distortion2 + + poly = np.array([xn, yn, xn**2, yn**2, xn * yn]) + + e_x, e_y = np.dot(coeffs, poly) + + xd = xn + e_x + yd = yn + e_y + + return np.array([xd, yd], dtype=dtype) \ No newline at end of file diff --git a/openpiv/calibration/pinhole_model/_epipolar_geom.py b/openpiv/calibration/pinhole_model/_epipolar_geom.py new file mode 100644 index 00000000..f90ca761 --- /dev/null +++ b/openpiv/calibration/pinhole_model/_epipolar_geom.py @@ -0,0 +1,133 @@ +import numpy as np + +from .._epipolar_utils import _line_intersect, _multi_line_intersect + + +__all__ = [ + "line_intersect", + "multi_line_intersect" +] + + +def line_intersect( + cam_1: "pinhole_model.camera", + cam_2: "pinhole_model.camera", + img_points_1: np.ndarray, + img_points_2: np.ndarray +): + """Calculate where two rays intersect. + + Using two cameras, calculate the world coordinates where two rays + intersect. This is done through an analytical solution based on + direction vectors and camera origins/translations. + + Parameters + ---------- + cam_1, cam_2 : pinhole_model.camera + An instance of a Pinhole camera. + img_points_1, img_points_2 : np.ndarray + Image coordinates stored in a ndarray structured like [x, y]'. + + Returns + ------- + coords : np.ndarray + The world coordinate that is nearest to the two rays intersecting. + dist : float + The minimum dinstance between the two rays. + + """ + cam_1._check_parameters() + cam_2._check_parameters() + + dtype1 = cam_1.dtype + dtype2 = cam_2.dtype + + # all cameras should have the same dtype + if dtype1 != dtype2: + raise ValueError( + "Dtypes between camera structures must match" + ) + + img_points_1 = np.array(img_points_1, dtype=dtype1) + img_points_2 = np.array(img_points_2, dtype=dtype2) + + r1 = cam_1._get_inverse_vector( + img_points_1 + ) + + r2 = cam_2._get_inverse_vector( + img_points_2 + ) + + t1 = cam_1.translation + t2 = cam_2.translation + + # TODO: move extention of dimensions into _line_intersect + return _line_intersect( + t1[:, np.newaxis], + r1, + t2[:, np.newaxis], + r2 + ) + + +def multi_line_intersect( + cameras: list, + img_points: list, +): + """Calculate where multiple rays intersect. + + Using at least two cameras, calculate the world coordinates where the + rays intersect. This is done through an least squares solution based on + direction vectors and camera origins/translations. + + Parameters + ---------- + cameras : list + A list of instances of Pinhole cameras. + img_points : list + A list of image coordinates for each canera structure. + + Returns + ------- + coords : np.ndarray + The world coordinate that is nearest to the intersection of all rays. + + """ + n_cams = len(cameras) + n_imgs = len(img_points) + + # make sure each camera has a set of images + if n_cams != n_imgs: + raise ValueError( + f"Camera - image size mismatch. Got {n_cams} cameras and " + + f"{n_imgs} images" + ) + + # check each camera structure + for cam in range(n_cams): + cameras[cam]._check_parameters() + + # all cameras should have the same dtype + dtype1 = cameras[0].dtype + for cam in range(1, n_cams): + dtype2 = cameras[cam].dtype + + if dtype1 != dtype2: + raise ValueError( + "Dtypes between camera structures must match" + ) + + lines = [] + for cam in range(n_cams): + points = np.array(img_points[cam], dtype=dtype1) + + r = cameras[cam]._get_inverse_vector( + points + ) + + t = cameras[cam].translation + + lines.append([t, r]) + + return _multi_line_intersect(lines, dtype1) \ No newline at end of file diff --git a/openpiv/calibration/pinhole_model/_minimization.py b/openpiv/calibration/pinhole_model/_minimization.py new file mode 100644 index 00000000..92cb9ac6 --- /dev/null +++ b/openpiv/calibration/pinhole_model/_minimization.py @@ -0,0 +1,187 @@ +import numpy as np +from scipy.optimize import minimize + +from ..calib_utils import get_reprojection_error, get_los_error +from .. import _cal_doc_utils + + +@_cal_doc_utils.docfiller +def _minimize_params( + self, + object_points: list, + image_points: list, + correct_focal: bool = False, + correct_distortion: bool = False, + max_iter: int = 1000, + iterations: int = 3 +): + """Minimize camera parameters. + + Minimize camera parameters using BFGS optimization. To do this, the + root mean square error (RMS error) is calculated for each iteration. + The set of parameters with the lowest RMS error is returned (which is + hopefully correct the minimum). + + Parameters + ---------- + %(object_points)s + %(image_points)s + correct_focal : bool + If true, minimize the focal point. + correct_distortion : bool + If true, minimize the distortion model. + max_iter : int + Maximum amount of iterations in Nelder-Mead minimization. + iterations : int + Number of iterations to perform. + + Returns + ------- + None + + Notes + ----- + When minimizing the camera parameters, it is important that the + parameters are estimated first before distortion correction. This allows + a better estimation of the camera parameters and distortion coefficients. + For instance, if one were to calibrate the camera intrinsic and distortion + coefficients before moving the camera to the lab apparatus, it would be + important to calibrate the camera parameters before the distortion model + to ensure a better convergence and thus, lower root mean square (RMS) errors. + This can be done in the following procedure: + + 1. Place the camera directly in front of a planar calibration plate. + + 2. Estimate camera parameters with out distortion correction. + + 3. Estimate distortion model coefficients and refine camera parameters. + Note: It may be best to use least squares minimization first. + + 4. Place camera in lab apparatus. + + 5. Calibrate camera again without distortion or intrinsic correction. + + A brief example is shown below. More can be found in the example PIV lab + experiments. + + On a side note, for a decent calibration to occur, at least 20 points are + needed. For attaining a rough estimate for marker detection purposes, at + least 9 points are needed (of course, this is excluding distortion correction). + + Examples + -------- + >>> import numpy as np + >>> from importlib_resources import files + >>> from openpiv.calibration import pinhole_model, calib_utils + + >> path_to_calib = files('openpiv.data').joinpath('test7/D_Cal.csv') + + >>> obj_x, obj_y, obj_z, img_x, img_y = np.loadtxt( + path_to_calib, + unpack=True, + skiprows=1, + usecols=range(5), + delimiter=',' + ) + + >>> cam = pinhole_model.camera( + 'cam1', + [4512, 800], + translation = [-340, 125, 554], + orientation = [0., 0, np.pi], + focal = [15310, 15310], + ) + + >>> cam.minimize_params( + [obj_x, obj_y, obj_z], + [img_x, img_y], + correct_focal = True, + correct_distortion = False, + iterations=5 + ) + + >>> cam.minimize_params( + [obj_x, obj_y, obj_z], + [img_x, img_y], + correct_focal = True, + correct_distortion = True, + iterations=5 + ) + + >> calib_utils.get_reprojection_error( + cam, + [obj_x, obj_y, obj_z], + [img_x0, img_y0] + ) + + >> 0.041367701972229325 + + """ + self._check_parameters() + + dtype = self.dtype + + object_points = np.array(object_points, dtype=dtype) + image_points = np.array(image_points, dtype=dtype) + + if object_points.shape[1] < 9: + raise ValueError( + "Too little points to calibrate" + ) + + if object_points.shape[1] != image_points.shape[1]: + raise ValueError( + "Object point image point size mismatch" + ) + + # For each iteration, calculate the RMS error of this function. The input is a numpy + # array to meet the requirements of scipy's minimization functions. + def func_to_minimize(x): + self.translation = x[0:3] + self.orientation = x[3:6] + self.principal = x[6:8] + + if correct_focal == True: + self.focal = x[8:10] + + self._get_rotation_matrix() + + if correct_distortion == True: + if self.distortion_model.lower() == "brown": + self.distortion1 = x[10:18] + else: + self.distortion2[0, :] = x[18:23] + self.distortion2[1, :] = x[23:28] + + RMS_error = get_reprojection_error( + self, + object_points, + image_points + ) + + return RMS_error + + # Create a numpy array since we cannot pass a dictionary to scipy's minimize function. + params_to_minimize = [ + self.translation, + self.orientation, + self.principal, + self.focal, + self.distortion1, + self.distortion2.ravel() + ] + + params_to_minimize = np.hstack( + params_to_minimize + ) + + # Peform multiple iterations to hopefully attain a better calibration. + for _ in range(iterations): + # Discard output of minimization as we are interested in the camera params dict. + res = minimize( + func_to_minimize, + params_to_minimize, + method="bfgs", + options={"maxiter": max_iter}, + jac = "2-point" + ) \ No newline at end of file diff --git a/openpiv/calibration/pinhole_model/_projection.py b/openpiv/calibration/pinhole_model/_projection.py new file mode 100644 index 00000000..84a88b99 --- /dev/null +++ b/openpiv/calibration/pinhole_model/_projection.py @@ -0,0 +1,325 @@ +import numpy as np + +from .. import _cal_doc_utils + + +_all__ = [ + "project_points", + "project_to_z" +] + + +def _normalize_world_points( + self, + object_points: np.ndarray +): + R = self.rotation + T = self.translation + dtype = self.dtype + + # transformation to camera coordinates + Wc = np.dot( + R.T, + object_points + ) - np.dot(R.T, T[:, np.newaxis]) + + # the camera coordinates + Wc_x = Wc[0, :] + Wc_y = Wc[1, :] + Wc_h = Wc[2, :] + + # normalize coordinates + Wn_x = Wc_x / Wc_h + Wn_y = Wc_y / Wc_h + + return np.array([Wn_x, Wn_y], dtype=dtype) + + +def _normalize_image_points( + self, + image_points: np.ndarray +): + fx, fy = self.focal + cx, cy = self.principal + dtype = self.dtype + + x, y = image_points + + # normalize image coordinates + Wn_x = (x - cx) / fx + Wn_y = (y - cy) / fy + + return np.array([Wn_x, Wn_y], dtype=dtype) + + +@_cal_doc_utils.docfiller +def _project_points( + self, + object_points: np.ndarray, + correct_distortion: bool = True +): + """Project object points to image points. + + Project object, or real world points, to image points. + + Parameters + ---------- + %(object_points)s + correct_distortion : bool + If true, perform distortion correction. + + Returns + ------- + %(x_img_coord)s + %(y_img_coord)s + + Examples + -------- + >>> import numpy as np + >>> from importlib_resources import files + >>> from openpiv.calibration import pinhole_model + + >>> path_to_calib = files('openpiv.data').joinpath('test7/D_Cal.csv') + + >>> obj_x, obj_y, obj_z, img_x, img_y = np.loadtxt( + path_to_calib, + unpack=True, + skiprows=1, + usecols=range(5), # get first 5 columns of data + delimiter=',' + ) + + >>> obj_points = np.array([obj_x[0:3], obj_y[0:3], obj_z[0:3]], dtype="float64") + >>> img_points = np.array([img_x[0:3], img_y[0:3]], dtype="float64") + + >>> cam = pinhole_model.camera( + 'cam1', + [4512, 800], + translation = [-340, 125, 554], + orientation = [0., 0, np.pi], + focal = [15310, 15310], + ) + + >>> cam.minimize_params( + [obj_x, obj_y, obj_z], + [img_x, img_y], + correct_focal = True, + correct_distortion = False, + iterations=5 + ) + + >>> cam.minimize_params( + [obj_x, obj_y, obj_z], + [img_x, img_y], + correct_focal = True, + correct_distortion = True, + iterations=5 + ) + + >>> cam.project_points( + obj_points + ) + array([[-44.18942498, -33.54150164, -22.85390086], + [ 89.60689939, 211.87910151, 334.5884999 ]]) + + >>> img_points + array([[-44.33764398, -33.67518587, -22.97467733], + [ 89.61102873, 211.8863641 , 334.5980555 ]]) + + """ + self._check_parameters() + + fx, fy = self.focal + cx, cy = self.principal + dtype = self.dtype + + object_points = np.array(object_points, dtype=dtype) + + Wn_x, Wn_y = self._normalize_world_points( + object_points + ) + + if correct_distortion == True: + if self.distortion_model.lower() == "brown": + Wn_x, Wn_y = self._undistort_points_brown( + Wn_x, + Wn_y + ) + else: + Wn_x, Wn_y = self._undistort_points_poly( + Wn_x, + Wn_y + ) + + # rescale coordinates + x = Wn_x * fx + cx + y = Wn_y * fy + cy + + return np.array([x, y], dtype=dtype) + + +@_cal_doc_utils.docfiller +def _get_inverse_vector( + self, + image_points: np.ndarray +): + """Get pixel to direction vector. + + Calculate a direction vector from a pixel. This vector can be used for + forward projection along a ray using y + ar where y is the origin of + the camera, a is the z plane along the ray, and r is the direction + vector. + + Parameters + ---------- + %(image_points)s + + Returns + ------- + dx : 1D np.ndarray + Direction vector for x-axis. + dy : 1D np.ndarray + Direction vector for y-axis. + dz : 1D np.ndarray + Direction vector for z-axis. + + Notes + ----- + The direction vector is not normalized. + + """ + R = self.rotation + dtype = self.dtype + + image_points = np.array(image_points, dtype=dtype) + + Wn_x, Wn_y = self._normalize_image_points( + image_points + ) + + if self.distortion_model.lower() == "brown": + Wn_x, Wn_y = self._distort_points_brown( + Wn_x, + Wn_y + ) + else: + Wn_x, Wn_y = self._distort_points_poly( + Wn_x, + Wn_y + ) + + # inverse rotation + dx, dy, dz = np.dot( + R, + [Wn_x, Wn_y, np.ones_like(Wn_x)] + ) + + return np.array([dx, dy, dz], dtype=dtype) + + +@_cal_doc_utils.docfiller +def _project_to_z( + self, + image_points: np.ndarray, + z: np.ndarray +): + """Project image points to world points. + + Project image points to world points at specified z-plane using a + closed form solution (when omiting distortion correction). This means + under ideal circumstances with no distortion, the forward project + coordinates would be accurate down to machine precision or numerical + round off errors. + + Parameters + ---------- + %(image_points)s + %(project_z)s + + Returns + ------- + %(x_lab_coord)s + %(y_lab_coord)s + %(z_lab_coord)s + + Examples + -------- + >>> import numpy as np + >>> from importlib_resources import files + >>> from openpiv.calibration import pinhole_model + + >>> path_to_calib = files('openpiv.data').joinpath('test7/D_Cal.csv') + + >>> obj_x, obj_y, obj_z, img_x, img_y = np.loadtxt( + path_to_calib, + unpack=True, + skiprows=1, + usecols=range(5), # get first 5 columns of data + delimiter=',' + ) + + >>> obj_points = np.array([obj_x[0:3], obj_y[0:3], obj_z[0:3]], dtype="float64") + >>> img_points = np.array([img_x[0:3], img_y[0:3]], dtype="float64") + + >>> cam = pinhole_model.camera( + 'cam1', + [4512, 800], + translation = [-340, 125, 554], + orientation = [0., 0, np.pi], + focal = [15310, 15310], + ) + + >>> cam.minimize_params( + [obj_x, obj_y, obj_z], + [img_x, img_y], + correct_focal = True, + correct_distortion = False, + iterations=5 + ) + + >>> cam.minimize_params( + [obj_x, obj_y, obj_z], + [img_x, img_y], + correct_focal = True, + correct_distortion = True, + iterations=5 + ) + + >>> ij = cam.project_points( + obj_points + ) + + >>> ij + array([[-44.18942498, -33.54150164, -22.85390086], + [ 89.60689939, 211.87910151, 334.5884999 ]]) + + >>> cam.project_to_z( + ij, + z=obj_points[2] + ) + array([[-104.99762952, -104.99828339, -104.99883109], + [ -14.99969016, -9.99966432, -4.99960968], + [ -10. , -10. , -10. ]]) + + >>> obj_points + array([[-105., -105., -105.], + [ -15., -10., -5.], + [ -10., -10., -10.]]) + + """ + self._check_parameters() + + dtype = self.dtype + + dx, dy, dz = self._get_inverse_vector( + image_points + ) + + tx, ty, tz = self.translation + + a = (z - tz) / dz + + X = a*dx + tx + Y = a*dy + ty + + return np.array([X, Y, np.zeros_like(X) + z], dtype=dtype) \ No newline at end of file diff --git a/openpiv/calibration/pinhole_model/_utils.py b/openpiv/calibration/pinhole_model/_utils.py new file mode 100644 index 00000000..7cbaea71 --- /dev/null +++ b/openpiv/calibration/pinhole_model/_utils.py @@ -0,0 +1,228 @@ +import numpy as np +from os.path import join +from typing import Tuple + +from .. import _cal_doc_utils + + +def _get_rotation_matrix(self): + """Calculate a rotation matrix for a camera. + + Calculate a rotation matrix for a camera. The matrix is a 3x3 numpy ndarray + like such: + + [ r1 r2 r3 ] + [ r4 r5 r6 ] + [ r7 r8 r9 ] + + where + + r1 = cos(tz) * cos(ty) + r2 = -sin(tz) * cos(ty) + r3 = sin(ty) + r4 = cos(tz) * sin(tx) * sin(ty) + sin(tz) * cos(tx) + r5 = cos(tz) * cos(tx) - sin(tz) * sin(tx) * sin(ty) + r6 = -sin(tx) * cos(ty) + r7 = sin(tz) * sin(tx) - cos(tz) * cos(tx) * sin(ty) + r8 = sin(tz) * cos(tx) * sin(ty) + cos(tz) * sin(tx) + r9 = cos(tx) * cos(ty) + + """ + self._check_parameters() + + # Orientation is composed of angles, or theta, for each axes. + # Theta for each dimensions is abbreviated as t. + tx, ty, tz = self.orientation + dtype = self.dtype + + # We compute the camera patrix based off of this website. + # https://support.pix4d.com/hc/en-us/articles/202559089-How-are-the-Internal-and-External-Camera-Parameters-defined + + rot_x = np.array( + [ + [1, 0, 0], + [0, np.cos(tx),-np.sin(tx)], + [0, np.sin(tx), np.cos(tx)] + ], + dtype=dtype + ) + + rot_y = np.array( + [ + [ np.cos(ty), 0, np.sin(ty)], + [ 0, 1, 0], + [-np.sin(ty), 0, np.cos(ty)] + ], + dtype=dtype + ) + + rot_z = np.array( + [ + [np.cos(tz),-np.sin(tz), 0], + [np.sin(tz), np.cos(tz), 0], + [ 0, 0, 1] + ], + dtype=dtype + ) + + rotation_matrix = np.dot( + np.dot(rot_x, rot_y), + rot_z + ) + + self.rotation = rotation_matrix + + +@_cal_doc_utils.docfiller +def _save_parameters( + self, + file_path: str, + file_name: str=None +): + """Save pinhole camera parameters. + + Save the pinhole camera parameters to a text file. + + Parameters + ---------- + file_path : str + File path where the camera parameters are saved. + file_name : str, optional + If specified, override the default file name. + + Returns + ------- + None + + """ + self._check_parameters() + + if file_name is None: + file_name = self.name + + full_path = join(file_path, file_name) + + with open(full_path, 'w') as f: + f.write(self.name + '\n') + + _r = '' + for i in range(2): + _r += str(self.resolution[i]) + ' ' + + f.write(_r + '\n') + + _t = '' + for i in range(3): + _t += str(self.translation[i]) + ' ' + + f.write(_t + '\n') + + _o = '' + for i in range(3): + _o += str(self.orientation[i]) + ' ' + + f.write(_o + '\n') + + f.write(self.distortion_model + '\n') + + _d1 = '' + for i in range(8): + _d1 += str(self.distortion1[i]) + ' ' + + f.write(_d1 + '\n') + + for i in range(2): + _d2 = '' + for j in range(5): + _d2 += str(self.distortion2[i, j]) + ' ' + + f.write(_d2 + '\n') + + _f = '' + for i in range(2): + _f += str(self.focal[i]) + ' ' + + f.write(_f + '\n') + + _p = '' + for i in range(2): + _p += str(self.principal[i]) + ' ' + + f.write(_p + '\n') + + f.write(self.dtype + '\n') + + +@_cal_doc_utils.docfiller +def _load_parameters( + self, + file_path: str, + file_name: str +): + """Load pinhole camera parameters. + + Load the pinhole camera parameters from a text file. + + Parameters + ---------- + file_path : str + File path where the camera parameters are saved. + file_name : str + Name of the file that contains the camera parameters. + + Returns + ------- + None + + """ + full_path = join(file_path, file_name) + + with open(full_path, 'r') as f: + + name = f.readline()[:-1] + + _r = f.readline()[:-2] + resolution = np.array([float(s) for s in _r.split()]) + + _t = f.readline()[:-2] + translation = np.array([float(s) for s in _t.split()]) + + _o = f.readline()[:-2] + orientation = np.array([float(s) for s in _o.split()]) + + distortion_model = f.readline()[:-1] + + _d1 = f.readline()[:-2] + distortion1 = np.array([float(s) for s in _d1.split()]) + + distortion2 = [] + for i in range(2): + _d2 = f.readline()[:-2] + distortion2.append(np.array([float(s) for s in _d2.split()])) + + distortion2 = np.array(distortion2, dtype = "float64") + + _f = f.readline()[:-2] + focal = np.array([float(s) for s in _f.split()]) + + _p = f.readline()[:-2] + principal = np.array([float(s) for s in _p.split()]) + + dtype = f.readline()[:-1] + + self.name = name + self.resolution = resolution + self.translation = translation + self.orientation = orientation + self.distortion_model = distortion_model + self.distortion1 = distortion1 + self.distortion2 = distortion2 + self.focal = focal + self.principal = principal + self.dtype = dtype + + # make sure all parameters are valid + self._check_parameters() + + # get the rotation matrix as needed + self._get_rotation_matrix() \ No newline at end of file diff --git a/openpiv/calibration/pinhole_model/_zang.py b/openpiv/calibration/pinhole_model/_zang.py new file mode 100644 index 00000000..160b81f2 --- /dev/null +++ b/openpiv/calibration/pinhole_model/_zang.py @@ -0,0 +1,122 @@ +import numpy as np + +from ..dlt_model import calibrate_dlt + + +__all__ = [ + "calibrate_intrinsics" +] + + +def _get_all_homography( + all_object_points: np.ndarray, + all_image_points: np.ndarray +): + if len(all_object_points) < 2: + raise ValueError( + "Too little planes to calibrate" + ) + + all_H = [] + + for i in range(len(all_image_points)): + H, err = calibrate_dlt( + all_object_points, + all_image_points[i], + enforce_coplanar=True + ) + all_H.append(H) + + return np.array(all_H, dtype="float64").reshape((len(all_H), 3, 3)) + + +def _get_Vij( + all_h: np.ndarray, + i: int, + j: int +): + v_ij = np.zeros((all_h.shape[0], 6), dtype="float64") + + v_ij[:, 0] = all_h[:, 0, i] * all_h[:, 0, j] + v_ij[:, 1] = all_h[:, 0, i] * all_h[:, 1, j] + all_h[:, 1, i] * all_h[:, 0, j] + v_ij[:, 2] = all_h[:, 1, i] * all_h[:, 1, j] + v_ij[:, 3] = all_h[:, 2, i] * all_h[:, 0, j] + all_h[:, 0, i] * all_h[:, 2, j] + v_ij[:, 4] = all_h[:, 2, i] * all_h[:, 1, j] + all_h[:, 1, i] * all_h[:, 2, j] + v_ij[:, 5] = all_h[:, 2, i] * all_h[:, 2, j] + + return v_ij + + +def _get_B( + all_h: np.ndarray +): + v_00 = _get_Vij(all_h, 0, 0) + v_01 = _get_Vij(all_h, 0, 1) + v_11 = _get_Vij(all_h, 1, 1) + + v = np.zeros((all_h.shape[0] * 2, 6), dtype = "float64") + + v[0::2, :] = v_01 + v[1::2, :] = v_00 - v_11 + + U, E, V = np.linalg.svd(v) + + b = V[-1, :] + + B0, B1, B2, B3, B4, B5 = b + + # Rearrage B to form B = K^-T K^-1 + B = np.array([[B0, B1, B3], + [B1, B2, B4], + [B3, B4, B5]]) + + return B + + +def _get_intrinsics( + B: np.ndarray +): + v0 = (B[0, 1] * B[0, 2] - B[0, 0] * B[1, 2]) / (B[0, 0] * B[1, 1] - B[0, 1]**2) + lambda_ = B[2, 2] - (B[0, 2]**2 + v0 * (B[0, 1] * B[0, 2] - B[0, 0] * B[1, 2])) / B[0, 0] + alpha = np.sqrt(lambda_ / B[0, 0]) + beta = np.sqrt((lambda_ * B[0, 0]) / (B[0, 0] * B[1, 1] - B[0, 1]**2)) + gamma = -(B[0, 1] * alpha**2 * beta) / lambda_ + u0 = (gamma * v0 / beta) - (B[0, 2] * alpha**2) / lambda_ + + return alpha, beta, u0, v0, gamma # <-- gamma is skew + + +def calibrate_intrinsics( + all_object_points: np.ndarray, + all_image_points: np.ndarray +): + """Intrinsic calibration using Zhang's method. + + Using multiple views of planar targets, calculate the intrinsic + parameters that best fits all views using a closed-form solution. + + Parameters + ---------- + all_object_points : np.ndarray + Lab coordinates with a structured like [[X, Y, Z]'] * number + of planes. + + all_image_points : np.ndarray + Image coordinates with a structured like [[x, y]'] * number + of planes. + + Returns + ------- + intrinsics : np.ndarray + 5 elements that contain camera intrinsic information and are in the + order as such: fx, fy, cx, cy, and gamma (skew). + + """ + all_h = _get_all_homography( + all_object_points, + all_image_points + ) + + B = _get_B(all_h) + + return _get_intrinsics(B) \ No newline at end of file diff --git a/openpiv/calibration/pinhole_model/readme_pinhole_model.md b/openpiv/calibration/pinhole_model/readme_pinhole_model.md new file mode 100644 index 00000000..7cb3fa43 --- /dev/null +++ b/openpiv/calibration/pinhole_model/readme_pinhole_model.md @@ -0,0 +1,142 @@ +""" +==================== +Pinhole Camera Model +==================== + +This module contains an implementation of the pinhole camera model. This +model is an approximation of how light rays are captured by a camera. Under +ideal circumstances, lab coordinates can be mapped to image sensor +coordinates (also known as pixel coordinates). However, cameras are not +usually ideal and are placed arbitrarily in the lab space. This means that +the lab coordinates have to be transformed into normalized camera +coordinates to remove this arbitrary translation. The normalized camera +coordinates are calculated as such: + +$$ P = +\begin{vmatrix} +X \\ Y \\ Z \\ +\end{vmatrix} +$$ + +$$ +\begin{vmatrix} +x_c \\ y_c \\ z_c +\end{vmatrix} += P * R^{-1} - R^{-1} * T +$$ + +$$ +x_n = \frac{x_c}{z_c} \\ +y_n = \frac{y_c}{z_c} +$$ + +where + +$$ +R = +\begin{vmatrix} +r_{11} & r_{12} & r_{13} \\ +r_{21} & r_{22} & r_{23} \\ +r_{31} & r_{32} & r_{33} \\ +\end{vmatrix} +$$ +and +$$ +T = +\begin{vmatrix} +T_x \\ T_y \\ T_z +\end{vmatrix} +$$ + +where letters denoting R and T define the 3x3 rotation matrix and the +translation vector respectively, which are commonly associated with +the extrinsic matrix. + +Since there are usually additional irregularities such as lens +distortion and intermediate medias, additional steps to mitigate them are +necessary. Without correcting these irregularities, results from triangulation +or tomographic reconstruction are often meaningless. To circumnavigate this +issue, two distortion models are implemented: polynomial and brown +distortion models. The brown distortion model covers nearly all tangential +and radial distortion components and is relatively simple. However, this +distortion model cannot account for scheimpflug lenses or intermediate +medias. Under these circumstances, it is wise to use the polynomial +distortion model. This distortion model utilized second degree polynomials +for distorting and undistorting normalized camera coordinates. Since we are +working with polynomials, most distortions can be minimized including the use +of scheimpflug lenses and distortions caused by intermediate medias. All +distortion correction methods are applied directly to the normalized camera +coordinates. + +Finally, the normalized pixel coordinates are scaled using the intrinsic +matrix. The intrinsic matrix is composed of focal depths (fx, fy), measured +in pixels, and the principal point (cx, cy). One can estimate the focal +depths of fx and fy by dividing the focal number in mm with its associated +pixel size. For instance, if a lens had a focal number of 20mm and the pixels +are 5 um, then the fx/fy would be approximately 20/0.005 = 4000 pixels. The +following transformation applies the intrinsic matrix to normalized camera +coordinates: + +$$ +\begin{vmatrix} +x \\ y \\ 1 +\end{vmatrix} = +\begin{vmatrix} +f_x & 0 & c_x \\ +0 & f_y & c_y \\ +0 & 0 & 1 +\end{vmatrix} * +\begin{vmatrix} +x_n \\ y_n \\ 1 +\end{vmatrix} +$$ + +Once a camera systen is calibrated on an individual basis, it may be +beneficial to calculate 3D triangulation errors. This requires projecting +pixels along a light ray to a specific Z-plane. Since the distortion model +operate with normalized camera coordinates, it is vital that the image +coordinates are properly normalized. + +$$ +\begin{vmatrix} +x_n \\ y_n \\ 1 +\end{vmatrix} = +\begin{vmatrix} +f_x & 0 & c_x \\ +0 & f_y & c_y \\ +0 & 0 & 1 \\ +\end{vmatrix}^{-1} * +\begin{vmatrix} +x \\ +y \\ +1 \\ +\end{vmatrix} +$$ + +After normalization, the camera coordinates are distorted and turned into +direction vectors. + +$$ +\begin{vmatrix} +dx \\ +dy \\ +dz \\ +\end{vmatrix} = R * +\begin{vmatrix} +x_n \\y_n \\ 1 +\end{vmatrix} +$$ + +Finally, the lab coordinates can be calculated as Ax + b where x is the +direction vector, A is the distance from the physical camera to the +projection plane, and b is the translation of the physical camera in +respect to the calibration markers. + +$$ +a = \frac{(Z - T_z)}{d_z} \\ +X = a*d_x + T_x \\ +Y = a*d_y + T_y \\ +Z = Z +$$ + +""" \ No newline at end of file diff --git a/openpiv/calibration/poly_model/__init__.py b/openpiv/calibration/poly_model/__init__.py new file mode 100644 index 00000000..98ee17c9 --- /dev/null +++ b/openpiv/calibration/poly_model/__init__.py @@ -0,0 +1,31 @@ +""" +======================= +Polynomial Camera Model +======================= + +This module contains an implementation of a polynomial camera model. This +model is implemented using 3rd order polynomials in the x and y axis and a +2nd order polynomial along the z-axis. This model can handle a multiplitude +of different distortions and is usually preferred if processing algorithms +later on do not heavily utilize triangulation. Additionally, it is important +that the calibration markers cover as much of the image(s) as possible to +limit artifacts from extrapolation. + +Functions +========= + camera - Create an instance of a Soloff camera model + multi_line_intersect - Using multiple lines, approximate their intersection + +Note +==== +It is important to only import the submodule and not the functions that +are in the submodules. Explicitly importing a function from this submodule +could cause conflicts between other camera models due to similar naming +conventions that are normally protected behind namespaces. + +""" +from ._camera import * +from ._epipolar_geom import * + + +__all__ = [s for s in dir() if not s.startswith("_")] \ No newline at end of file diff --git a/openpiv/calibration/poly_model/_camera.py b/openpiv/calibration/poly_model/_camera.py new file mode 100644 index 00000000..823e4905 --- /dev/null +++ b/openpiv/calibration/poly_model/_camera.py @@ -0,0 +1,71 @@ +import numpy as np +from typing import Tuple + +from ._check_params import _check_parameters +from ._minimization import _minimize_params +from ._projection import _project_points, _project_to_z +from ._utils import _save_parameters, _load_parameters + + +__all__ = ["camera"] + + +class camera(object): + """An instance of a polynomial camera model. + + Create an instance of a polynomial camera. The polynomial camera model + is based on the Soloff camera model where a polymial mapping function + is created with third order polynomials in the x- and y-axis and a second + order polynomial along the z-axis. The polynomial camera models also has + a DLT-based estimator for 3D point triangulation for faster convergence. + + Attributes + ---------- + cam_name : str + Name of camera. + resolution : tuple[int, int] + Resolution of camera in x and y axes respectively. + poly_wi : np.ndarray + 19 coefficients for world to image polynomial calibration in [x, y]'. + poly_iw : np.ndarray + 19 coefficients for image to world polynomial calibration in [X, Y, Z]'. + dlt : np.ndarray + 12 coefficients for direct linear transformation. + dtype : str + The dtype used in the projections. + + Methods + ------- + minimize_params + project_points + project_to_z + save_parameters + load_parameters + + """ + def __init__( + self, + name: str, + resolution: Tuple[int, int], + poly_wi: np.ndarray=np.ones((2,19), dtype="float64").T, + poly_iw: np.ndarray=np.ones((3,19), dtype="float64").T, + dlt: np.ndarray=np.eye(3, 4), + dtype: str="float64" + ): + self.name = name + self.resolution = resolution + self.poly_wi = poly_wi + self.poly_iw = poly_iw + self.dlt = dlt + self.dtype = dtype + + # method definitions + camera._check_parameters = _check_parameters + camera.minimize_params = _minimize_params + camera.project_points = _project_points + camera.project_to_z = _project_to_z + camera.save_parameters = _save_parameters + camera.load_parameters = _load_parameters + + # check camera params at init + self._check_parameters() \ No newline at end of file diff --git a/openpiv/calibration/poly_model/_check_params.py b/openpiv/calibration/poly_model/_check_params.py new file mode 100644 index 00000000..d3f1d1fa --- /dev/null +++ b/openpiv/calibration/poly_model/_check_params.py @@ -0,0 +1,60 @@ +import numpy as np + + +def _check_parameters(self): + """Check camera parameters""" + + if type(self.name) != str: + raise ValueError( + "Camera name must be a string" + ) + + if len(self.resolution) != 2: + raise ValueError( + "Resolution must be a two element tuple" + ) + + if len(self.poly_wi.shape) != 2: + raise ValueError( + "World to image polynomial coefficients must be 2 dimensional." + ) + + if self.poly_wi.shape[0] != 19: + raise ValueError( + "World to image polynomial coefficients must be ordered in [x, y]'" + ) + + if self.poly_wi.shape[1] != 2: + raise ValueError( + "There must be 19 coefficients in the world to image polynomial" + ) + + if len(self.poly_iw.shape) != 2: + raise ValueError( + "Image to world polynomial coefficients must be 2 dimensional." + ) + + if self.poly_iw.shape[0] != 19: + raise ValueError( + "Image to world polynomial coefficients must be ordered in [x, y]'" + ) + + if self.poly_iw.shape[1] != 3: + raise ValueError( + "There must be 19 coefficients in the image to world polynomial" + ) + + if len(self.dlt.shape) != 2: + raise ValueError( + "DLT coefficients must be 2 dimensional." + ) + + if self.dlt.shape != (3, 4): + raise ValueError( + "DLT coefficients must be of shape (3, 4)" + ) + + if self.dtype not in ["float32", "float64"]: + raise ValueError( + "Dtype is not supported for camera calibration" + ) \ No newline at end of file diff --git a/openpiv/calibration/poly_model/_epipolar_geom.py b/openpiv/calibration/poly_model/_epipolar_geom.py new file mode 100644 index 00000000..14c2b7bc --- /dev/null +++ b/openpiv/calibration/poly_model/_epipolar_geom.py @@ -0,0 +1,397 @@ +import numpy as np +from typing import Tuple + +from ..dlt_model import (camera as dlt_camera, + line_intersect as dlt_line_intersect) + + +__all__ = [ + "multi_line_intersect" +] + + +def _dFdx( + cam: "poly_model.camera", + object_points: np.ndarray +) -> np.ndarray: + dtype = cam.dtype + a = cam.poly_wi + + object_points = np.array(object_points, dtype=dtype) + + X = object_points[0] + Y = object_points[1] + Z = object_points[2] + + polynomial_dx = np.array( + [ + np.ones_like(X), + Y, Z, + 2*X, + 3*X*X, 2*X*Y, 2*X*Z, + Y*Y, + Z*Z, Y*Z + ], + dtype=dtype + ).T + + polynomial_dx_a = np.array( + [ + a[1, :], + a[4, :], a[5, :], + a[7, :], + a[10, :], a[11, :], a[12, :], + a[14, :], + a[16, :], a[18, :] + ], + dtype=dtype + ) + + return np.dot( + polynomial_dx, + polynomial_dx_a + ).T + + +def _dFdy( + cam: "poly_model.camera", + object_points: np.ndarray +) -> np.ndarray: + dtype = cam.dtype + a = cam.poly_wi + + object_points = np.array(object_points, dtype=dtype) + + X = object_points[0] + Y = object_points[1] + Z = object_points[2] + + polynomial_dy = np.array( + [ + np.ones_like(Y), + X, Z, + 2*Y, + X*X, + 3*Y*Y, 2*Y*X, 2*Y*Z, + Z*Z, X*Z + ], + dtype=dtype + ).T + + polynomial_dy_a = np.array( + [ + a[2, :], + a[4, :], a[6, :], + a[8, :], + a[11, :], + a[13, :], a[14, :], a[15, :], + a[17, :], a[18, :] + ], + dtype=dtype + ) + + return np.dot( + polynomial_dy, + polynomial_dy_a + ).T + + +def _dFdz( + cam: "poly_model.camera", + object_points: np.ndarray +) -> np.ndarray: + dtype = cam.dtype + a = cam.poly_wi + + object_points = np.array(object_points, dtype=dtype) + + X = object_points[0] + Y = object_points[1] + Z = object_points[2] + + polynomial_dz = np.array( + [ + np.ones_like(Y), + X, Y, + 2*Z, + X*X, + Y*Y, + 2*X*Z, 2*Y*Z, X*Y + ], + dtype=dtype + ).T + + polynomial_dz_a = np.array( + [ + a[3, :], + a[5, :], a[6, :], + a[9, :], + a[12, :], + a[15, :], + a[16, :], a[17, :], a[18, :] + ], + dtype=dtype + ) + + return np.dot( + polynomial_dz, + polynomial_dz_a + ).T + + +def _refine_jac3D( + cams: list, + object_points: np.ndarray +) -> np.ndarray: + dtype = cams[0].dtype + + object_points = np.array(object_points, dtype=dtype) + + # if a 1D array is given, extend it + if len(object_points.shape) == 1: + object_points = object_points[:, np.newaxis] + + n_points = object_points.shape[1] + n_cams = len(cams) + + jac = np.zeros([n_points, n_cams*2, 3], dtype=dtype) + + for i in range(n_cams): + xdx, ydx = _dFdx(cams[i], object_points) + xdy, ydy = _dFdy(cams[i], object_points) + xdz, ydz = _dFdz(cams[i], object_points) + + jac[:, i*2, :] = np.array([xdx, xdy, xdz], dtype=dtype).T + jac[:, (i*2)+1, :] = np.array([ydx, ydy, ydz], dtype=dtype).T + + return jac + + +def _refine_func3D( + cams: list, + object_points: np.ndarray, + image_points: list +) -> np.ndarray: + dtype = cams[0].dtype + + n_cams = len(cams) + + # make sure each camera has a pair of image points + assert(n_cams == len(image_points)) + + # if a 1D array is given, extend it + if len(object_points.shape) == 1: + object_points = object_points[:, np.newaxis] + + if len(image_points[0].shape) == 1: + for i in range(len(image_points)): + image_points[i] = image_points[i][:, np.newaxis] + + n_points = object_points.shape[1] + + residuals = np.zeros([n_points, n_cams, 2], dtype=dtype) + + for i in range(n_cams): + res = cams[i].project_points( + object_points + ) - image_points[i] + + residuals[:, i, :] = res.T + + return residuals.reshape((n_points, n_cams*2)) + + +# TODO: move this function somewhere else? +def _minimize_gradient( + jac: np.ndarray, + residual: np.ndarray, + object_point: np.ndarray +): + object_point += np.linalg.lstsq(jac, -residual, rcond=None)[0] + + return object_point + + +def _refine_pos3D( + cams: list, + object_points: np.ndarray, + image_points: list, + iterations: int=3 +): + dtype = cams[0].dtype + n_cams = len(cams) + + # if a 1D array is given, extend it + if len(object_points.shape) == 1: + object_points = object_points[:, np.newaxis] + + if len(image_points[0].shape) == 1: + for i in range(len(image_points)): + image_points[i] = image_points[i][:, np.newaxis] + + n_points = object_points.shape[1] + + new_object_points = np.zeros([3, n_points], dtype=dtype) + new_object_points[:, :] = object_points + + # TODO: I bet this loop is hellish slow for a large number of particles + for i in range(iterations): + jac = _refine_jac3D( + cams, + new_object_points + ) + + residuals = _refine_func3D( + cams, + new_object_points, + image_points, + ) + + for particle in range(n_points): + img_points = [] + for cam in range(n_cams): + img_points.append(image_points[cam][:, particle]) + + obj_point = new_object_points[:, particle] + + new_object_points[:, particle] = _minimize_gradient( + jac[particle], + residuals[particle], + obj_point + ).T + + return new_object_points + + +def _estimate_pos( + cams: list, + image_points: list +): + # only need two cameras for analytical solution + cam1 = dlt_camera( + "dummy", + resolution = cams[0].resolution, + coeffs = cams[0].dlt, + dtype = cams[0].dtype + ) + + cam2 = dlt_camera( + "dummy", + resolution = cams[1].resolution, + coeffs = cams[1].dlt, + dtype = cams[1].dtype + ) + + # TODO: Should we use DLT's multi_line_intersect? + return dlt_line_intersect( + cam1, + cam2, + image_points[0], + image_points[1] + )[0] + + +# Now the good part starts :) +def multi_line_intersect( + cameras: list, + image_points: list, + init_pos: Tuple[float, float, float]=None, + iterations: int=3 +): + """Estimate 3D positions using a gradient descent algorithm. + + Using an approximated initial position, optimize the particle locations + such that the residuals between the image points and the projected + object points are minimized. This is performed by calculating an + analytical solution for the derivatives of the projection function and + finding a least squares solution by iteratively updating each point until + a specified amount of iterations have been completed. The least squares + solution is performed via SVD, making it robust to noise and artifacts. + This is necessary since the intitial particle positions are approximated + using the driect linear transforms (DLT) algorithm which ignores + all distortion artifacts. + + Parameters + ---------- + cameras : list + A list of instances of polynomial cameras. + img_points : list + A list of image coordinates for each canera structure. + init_pos : list, optional + An initial position for all particles given by a tuple of three + floats. The ordering of the tuple is (X, Y, Z) in world coordinates + and should be the center of the volume (e.g., (0, 0, 0)). If not + given, the particle positions are approximated using a DLT-based + algorithm. + iterations : int, optional + The number of iterations each object point recieves. + + Returns + ------- + coords : np.ndarray + The world coordinate that mininmize all projection residuals. + + References + ---------- + .. [Herzog] Herzog, S., Schiepel, D., Guido, I. et al. A Probabilistic + Particle Tracking Framework for Guided and Brownian Motion + Systems with High Particle Densities. SN COMPUT. SCI. 2, 485 + (2021). https://doi.org/10.1007/s42979-021-00879-z + + """ + n_cams = len(cameras) + n_imgs = len(image_points) + + # make sure each camera has a set of images + if n_cams != n_imgs: + raise ValueError( + f"Camera - image size mismatch. Got {n_cams} cameras and " + + f"{n_imgs} images" + ) + + # check each camera structure + for cam in range(n_cams): + cameras[cam]._check_parameters() + + # all cameras should have the same dtype + dtype1 = cameras[0].dtype + for cam in range(1, n_cams): + dtype2 = cameras[cam].dtype + + if dtype1 != dtype2: + raise ValueError( + "Dtypes between camera structures must match" + ) + + # TODO: we should also check for individual image mismatches + # if a 1D array is given, extend it + if len(image_points[0].shape) == 1: + for i in range(len(image_points)): + image_points[i] = image_points[i][:, np.newaxis] + + if init_pos is not None: + n_particles = image_points[0].shape[1] + + object_points = np.zeros( + [3, n_particles], + dtype=dtype1 + ) + + object_points[0, :] = init_pos[0] + object_points[1, :] = init_pos[1] + object_points[2, :] = init_pos[2] + + else: + object_points = _estimate_pos( + cameras, + image_points + ) + + object_points = _refine_pos3D( + cameras, + object_points, + image_points, + iterations=iterations + ) + + return object_points \ No newline at end of file diff --git a/openpiv/calibration/poly_model/_minimization.py b/openpiv/calibration/poly_model/_minimization.py new file mode 100644 index 00000000..13c34e00 --- /dev/null +++ b/openpiv/calibration/poly_model/_minimization.py @@ -0,0 +1,169 @@ +import numpy as np +from scipy.optimize import least_squares + +from ..dlt_model import calibrate_dlt +from .. import _cal_doc_utils + + +def _refine_poly( + poly_coeffs: np.ndarray, + polynomial: np.ndarray, + expected: np.ndarray +): + # use lambdas since it keeps the function definitions local + def refine_func(coeffs): + projected = np.dot(polynomial, coeffs) + return projected - expected + + return least_squares( + refine_func, + poly_coeffs, + method="trf" # more modern lm algorithm + ).x + + +@_cal_doc_utils.docfiller +def _minimize_params( + self, + object_points: list, + image_points: list, +): + """Minimize polynomials. + + Minimize polynomials using Least Squares minimization. + + Parameters + ---------- + %(object_points)s + %(image_points)s + + Examples + -------- + >>> import numpy as np + >>> from importlib_resources import files + >>> from openpiv.calibration import poly_model, calib_utils + + >>> path_to_calib = files('openpiv.data').joinpath('test7/D_Cal.csv') + + >>> obj_x, obj_y, obj_z, img_x, img_y = np.loadtxt( + path_to_calib, + unpack=True, + skiprows=1, + usecols=range(5), + delimiter=',' + ) + + >>> obj_points = np.array([obj_x[0:3], obj_y[0:3], obj_z[0:3]], dtype="float64") + >>> img_points = np.array([img_x[0:3], img_y[0:3]], dtype="float64") + + >>> cam = poly_model.camera( + 'cam1', + [4512, 800] + ) + + >>> cam.minimize_params( + [obj_x, obj_y, obj_z], + [img_x, img_y] + ) + + >>> calib_utils.get_reprojection_error( + cam, + [obj_x, obj_y, obj_z], + [img_x, img_y] + ) + 0.16553632335727653 + + """ + self._check_parameters() + + dtype = self.dtype + + object_points = np.array(object_points, dtype=dtype) + image_points = np.array(image_points, dtype=dtype) + + if object_points.shape[1] < 19: + raise ValueError( + "Too little points to calibrate" + ) + + if object_points.shape[1] != image_points.shape[1]: + raise ValueError( + "Object point image point size mismatch" + ) + + x = image_points[0] + y = image_points[1] + + X = object_points[0] + Y = object_points[1] + Z = object_points[2] + + polynomial_wi = np.array( + [ + np.ones_like(X), + X, Y, Z, + X*Y, X*Z, Y*Z, + X**2, Y**2, Z**2, + X**3, X*X*Y, X*X*Z, + Y**3, X*Y*Y, Y*Y*Z, + X*Z*Z, Y*Z*Z, X*Y*Z + ], + dtype=dtype + ).T + + # in the future, break this into three Z subvolumes to further reduce errors. + polynomial_iw = np.array( + [ + np.ones_like(x), + x, y, Z, + x*y, x*Z, y*Z, + x**2, y**2, Z**2, + x**3, x*x*y, x*x*Z, + y**3, x*y*y, y*y*Z, + x*Z*Z, y*Z*Z, x*y*Z + ], + dtype=dtype + ).T + + + # world to image (forward projection) + coeff_wi = np.zeros([19, 2], dtype=dtype) + + for i in range(2): + coeff_wi[:, i] = np.linalg.lstsq( + polynomial_wi, + image_points[i], + rcond=None + )[0] + + coeff_wi[:, i] = _refine_poly( + coeff_wi[:, i], + polynomial_wi, + image_points[i] + ) + + # image to world (back projection) + coeff_iw = np.zeros([19, 3], dtype=dtype) + + for i in range(3): + coeff_iw[:, i] = np.linalg.lstsq( + polynomial_iw, + object_points[i], + rcond=None + )[0] + + coeff_iw[:, i] = _refine_poly( + coeff_iw[:, i], + polynomial_iw, + object_points[i] + ) + + # DLT estimator + dlt_matrix, _residual = calibrate_dlt( + object_points, + image_points + ) + + self.poly_wi = coeff_wi + self.poly_iw = coeff_iw + self.dlt = dlt_matrix \ No newline at end of file diff --git a/openpiv/calibration/poly_model/_projection.py b/openpiv/calibration/poly_model/_projection.py new file mode 100644 index 00000000..18ad0949 --- /dev/null +++ b/openpiv/calibration/poly_model/_projection.py @@ -0,0 +1,200 @@ +import numpy as np + +from .. import _cal_doc_utils + + +@_cal_doc_utils.docfiller +def _project_points( + self, + object_points: np.ndarray +): + """Project object points to image points. + + Project object, or real world points, to image points. + + Parameters + ---------- + %(object_points)s + + Returns + ------- + %(x_img_coord)s + %(y_img_coord)s + + Examples + -------- + >>> import numpy as np + >>> from importlib_resources import files + >>> from openpiv.calibration import poly_model + + >>> path_to_calib = files('openpiv.data').joinpath('test7/D_Cal.csv') + + >>> obj_x, obj_y, obj_z, img_x, img_y = np.loadtxt( + path_to_calib, + unpack=True, + skiprows=1, + usecols=range(5), # get first 5 columns of data + delimiter=',' + ) + + >>> obj_points = np.array([obj_x[0:3], obj_y[0:3], obj_z[0:3]], dtype="float64") + >>> img_points = np.array([img_x[0:3], img_y[0:3]], dtype="float64") + + >>> cam = poly_model.camera( + 'cam1', + [4512, 800] + ) + + >>> cam.minimize_params( + [obj_x, obj_y, obj_z], + [img_x, img_y] + ) + + >>> cam.project_points( + obj_points + ) + array([[-44.24281474, -33.56231972, -22.84229244], + [ 89.63444964, 211.90372246, 334.60601499]]) + + >>> img_points + array([[-44.33764398, -33.67518587, -22.97467733], + [ 89.61102873, 211.8863641 , 334.5980555 ]]) + + """ + self._check_parameters() + + dtype = self.dtype + + object_points = np.array(object_points, dtype=dtype) + + X = object_points[0] + Y = object_points[1] + Z = object_points[2] + + polynomial_wi = np.array( + [ + np.ones_like(X), + X, Y, Z, + X*Y, X*Z, Y*Z, + X**2, Y**2, Z**2, + X**3, X*X*Y, X*X*Z, + Y**3, X*Y*Y, Y*Y*Z, + X*Z*Z, Y*Z*Z, X*Y*Z + ], + dtype=dtype + ).T + + return np.dot( + polynomial_wi, + self.poly_wi + ).T + + +@_cal_doc_utils.docfiller +def _project_to_z( + self, + image_points: np.ndarray, + z: float +): + """Project image points to world points. + + Project image points to world points at specified z-plane. + + Parameters + ---------- + %(image_points)s + %(project_z)s + + Returns + ------- + %(x_lab_coord)s + %(y_lab_coord)s + %(z_lab_coord)s + + Examples + -------- + >>> import numpy as np + >>> from importlib_resources import files + >>> from openpiv.calibration import poly_model + + >>> path_to_calib = files('openpiv.data').joinpath('test7/D_Cal.csv') + + >>> obj_x, obj_y, obj_z, img_x, img_y = np.loadtxt( + path_to_calib, + unpack=True, + skiprows=1, + usecols=range(5), # get first 5 columns of data + delimiter=',' + ) + + >>> obj_points = np.array([obj_x[0:3], obj_y[0:3], obj_z[0:3]], dtype="float64") + >>> img_points = np.array([img_x[0:3], img_y[0:3]], dtype="float64") + + >>> cam = poly_model.camera( + 'cam1', + [4512, 800] + ) + + >>> cam.minimize_params( + [obj_x, obj_y, obj_z], + [img_x, img_y] + ) + + >>> ij = cam.project_points( + obj_points + ) + + >>> ij + array([[-44.24281474, -33.56231972, -22.84229244], + [ 89.63444964, 211.90372246, 334.60601499]]) + + >>> cam.project_to_z( + ij, + z=obj_points[2] + ) + array([[-105.0088358 , -105.00967895, -105.01057378], + [ -15.0022918 , -10.00166811, -5.00092353], + [ -10.00000004, -10.00000003, -10.00000002]]) + + >>> obj_points + array([[-105., -105., -105.], + [ -15., -10., -5.], + [ -10., -10., -10.]]) + + """ + self._check_parameters() + + dtype = self.dtype + + image_points = np.array(image_points, dtype=dtype) + + x = image_points[0] + y = image_points[1] + + if isinstance(z, np.ndarray): + Z = np.array(z, dtype=dtype) + elif isinstance(z, (float, int)): + Z = np.zeros_like(x) + z + else: + raise ValueError( + "Unsupported value entered for `z`. `z` must be either a scalar " + + "or numpy ndarray" + ) + + polynomial_iw = np.array( + [ + np.ones_like(x), + x, y, Z, + x*y, x*Z, y*Z, + x**2, y**2, Z**2, + x**3, x*x*y, x*x*Z, + y**3, x*y*y, y*y*Z, + x*Z*Z, y*Z*Z, x*y*Z + ], + dtype=dtype + ).T + + return np.dot( + polynomial_iw, + self.poly_iw + ).T \ No newline at end of file diff --git a/openpiv/calibration/poly_model/_utils.py b/openpiv/calibration/poly_model/_utils.py new file mode 100644 index 00000000..b3ef3cb6 --- /dev/null +++ b/openpiv/calibration/poly_model/_utils.py @@ -0,0 +1,117 @@ +import numpy as np +from os.path import join +from typing import Tuple + + +def _save_parameters( + self, + file_path: str, + file_name: str=None +): + """Save polynomial camera parameters. + + Save the polynomial camera parameters to a text file. + + Parameters + ---------- + file_path : str + File path where the camera parameters are saved. + file_name : str, optional + If specified, override the default file name. + + Returns + ------- + None + + """ + if file_name is None: + file_name = self.name + + full_path = join(file_path, file_name) + + with open(full_path, 'w') as f: + f.write(self.name + '\n') + + _r = '' + for i in range(2): + _r += str(self.resolution[i]) + ' ' + + f.write(_r + '\n') + + for i in range(19): + _d2 = '' + for j in range(2): + _d2 += str(self.poly_wi[i, j]) + ' ' + + f.write(_d2 + '\n') + + for i in range(19): + _d2 = '' + for j in range(3): + _d2 += str(self.poly_iw[i, j]) + ' ' + + f.write(_d2 + '\n') + + for i in range(3): + _c = '' + for j in range(self.dlt.shape[1]): + _c += str(self.dlt[i, j]) + ' ' + + f.write(_c + '\n') + + f.write(self.dtype + '\n') + + +def _load_parameters( + self, + file_path: str, + file_name: str +): + """Load polynomial camera parameters. + + Load the polynomial camera parameters from a text file. + + Parameters + ---------- + file_path : str + File path where the camera parameters are saved. + file_name : str + Name of the file that contains the camera parameters. + + """ + full_path = join(file_path, file_name) + + with open(full_path, 'r') as f: + + name = f.readline()[:-1] + + _r = f.readline()[:-2] + resolution = np.array([float(s) for s in _r.split()]) + + poly_wi = [] + for i in range(19): + _d2 = f.readline()[:-2] + poly_wi.append(np.array([float(s) for s in _d2.split()])) + + poly_iw = [] + for i in range(19): + _d2 = f.readline()[:-2] + poly_iw.append(np.array([float(s) for s in _d2.split()])) + + dlt = [] + for i in range(3): + _c = f.readline()[:-2] + dlt.append(np.array([float(s) for s in _c.split()])) + + dtype = f.readline()[:-1] + + poly_wi = np.array(poly_wi, dtype=dtype) + poly_iw = np.array(poly_iw, dtype=dtype) + dlt = np.array(dlt, dtype=dtype) + + self.name = name + self.resolution = resolution + self.poly_wi = poly_wi + self.poly_iw = poly_iw + self.dlt = dlt + self.dtype = dtype \ No newline at end of file diff --git a/openpiv/data/test7/D_Cal.csv b/openpiv/data/test7/D_Cal.csv new file mode 100644 index 00000000..f384461a --- /dev/null +++ b/openpiv/data/test7/D_Cal.csv @@ -0,0 +1,1506 @@ +x,y,z,Xcam0,Ycam0,Xcam1,Ycam1,Xcam2,Ycam2,Xcam3,Ycam3 +-105,-15,-10,-44.33764398,89.61102873,313.0399753,109.2077229,221.9885392,61.49948848,40.21196404,-49.85743431 +-105,-10,-10,-33.67518587,211.8863641,297.9770332,210.8318921,237.2767446,164.6434274,29.35719864,74.62324968 +-105,-5,-10,-22.97467733,334.5980555,282.8694386,312.7573161,252.5194949,267.4806972,18.54130868,198.6581165 +-105,0,-10,-12.23591431,457.7484431,267.7169925,414.9853365,267.7169925,370.0126635,7.764085685,322.2495569 +-105,5,-10,-1.458691318,581.3398835,252.5194949,517.5173028,282.8694386,472.2406839,-2.97467733,445.3999445 +-105,10,-10,9.357198644,705.3747503,237.2767446,620.3545726,297.9770332,574.1661079,-13.67518587,568.1116359 +-105,15,-10,20.21196404,829.8554343,221.9885392,723.4985115,313.0399753,675.7902771,-24.33764398,690.3869713 +-100,-15,-10,75.14593866,90.02714407,396.3208472,108.9176877,305.6467403,59.29077543,160.6636727,-46.67731261 +-100,-10,-10,85.96878297,211.7137158,381.32078,210.9521586,320.8720468,162.8573839,149.6465812,77.19327531 +-100,-5,-10,96.83006353,333.8324452,366.2760665,313.2903249,336.0519013,266.1148165,138.6687554,200.6223813 +-100,0,-10,107.7299855,456.3856386,351.1865073,415.9335445,351.1865073,369.0644555,127.7299855,323.6123614 +-100,5,-10,118.6687554,579.3756187,336.0519013,518.8831835,366.2760665,471.7076751,116.8300635,446.1655548 +-100,10,-10,129.6465812,702.8047247,320.8720468,622.1406161,381.32078,574.0458414,105.968783,568.2842842 +-100,15,-10,140.6636727,826.6753126,305.6467403,725.7072246,396.3208472,676.0803123,95.14593866,689.9708559 +-95,-15,-10,193.4849841,90.43927342,480.2766338,108.625302,389.9890288,57.06400136,279.9492495,-43.52797869 +-95,-10,-10,204.4646566,211.5427242,465.3409586,211.0734011,405.149896,161.0567576,268.7734725,79.7384639 +-95,-5,-10,215.4831348,333.0741946,450.3606483,313.8276661,420.2653191,264.7378002,257.6373345,202.5676969 +-95,0,-10,226.5406249,455.0359578,435.3355023,416.8894714,435.3355023,368.1085286,246.5406249,324.9620422 +-95,5,-10,237.6373345,577.4303031,420.2653191,520.2601998,450.3606483,471.1703339,235.4831348,446.9238054 +-95,10,-10,248.7734725,700.2595361,405.149896,623.9412424,465.3409586,573.9245989,224.4646566,568.4552758 +-95,15,-10,259.9492495,823.5259787,389.9890288,727.9339986,480.2766338,676.372698,213.4849841,689.5587266 +-90,-15,-10,310.6958594,90.84747379,564.9155727,108.3305372,475.0238301,54.81894381,398.0855475,-40.40898761 +-90,-10,-10,321.8288816,211.3733656,550.0458378,211.1956317,490.1186859,159.241369,386.7546431,82.25917375 +-90,-5,-10,333.0010633,332.3231981,535.1314834,314.3693928,505.1681105,263.3495116,375.4637349,204.4943361 +-90,0,-10,344.2126116,453.699212,520.1723085,417.8532118,520.1723085,367.1447882,364.2126116,326.298788 +-90,5,-10,355.4637349,575.5036639,505.1681105,521.6484884,535.1314834,470.6286072,353.0010633,447.6748019 +-90,10,-10,366.7546431,697.7388262,490.1186859,625.756631,550.0458378,573.8023683,341.8288816,568.6246344 +-90,15,-10,378.0855475,820.4069876,475.0238301,730.1790562,564.9155727,676.6674628,330.6958594,689.1505262 +-85,-15,-10,426.7946208,91.2518011,650.2460363,108.033364,560.7597083,52.55537669,515.0890964,-37.31990295 +-85,-10,-10,438.077592,211.2056167,635.4438215,211.3188625,575.7869485,157.411036,503.6065425,84.75575633 +-85,-5,-10,449.4000613,331.5793521,620.5970078,314.9155589,590.768775,261.9498117,492.1643266,206.4025666 +-85,0,-10,460.7622364,452.3752162,605.7053933,418.8248618,605.7053933,366.1731382,480.7622364,327.6227838 +-85,5,-10,472.1643266,573.5954334,590.768775,523.0481883,620.5970078,470.0824411,469.4000613,448.4186479 +-85,10,-10,483.6065425,695.2422437,575.7869485,627.586964,635.4438215,573.6791375,458.077592,568.7923833 +-85,15,-10,495.0890964,817.3179029,560.7597083,732.4426233,650.2460363,676.964636,446.7946208,688.7461989 +-80,-15,-10,541.7970215,91.6523102,736.2765343,107.7337529,647.2053696,50.27307014,630.9761105,-34.26029663 +-80,-10,-10,553.2266166,211.0394547,721.5434516,211.4431058,662.1633566,155.5655732,619.3453071,87.22855638 +-80,-5,-10,564.6960336,330.842555,706.7657957,315.4662192,677.0759526,260.5385593,607.7551682,208.2926511 +-80,0,-10,576.2054808,451.0637888,691.943364,419.8045193,691.943364,365.1934807,596.2054808,328.9342112 +-80,5,-10,587.7551682,571.7053489,677.0759526,524.4594407,706.7657957,469.5317808,584.6960336,449.155445 +-80,10,-10,599.3453071,692.7694436,662.1633566,629.4324268,721.5434516,573.5548942,573.2266166,568.9585453 +-80,15,-10,610.9761105,814.2582966,647.2053696,734.7249299,736.2765343,677.2642471,561.7970215,688.3456898 +-75,-15,-10,655.7185184,92.04905494,823.0157166,107.4316737,734.3696646,47.97179049,745.7624962,-31.2297487 +-75,-10,-10,667.2914862,210.8748571,808.3534113,211.568374,749.2567271,153.7047922,733.9867671,89.67791212 +-75,-5,-10,678.9045851,330.112707,793.6465635,316.0214294,764.0984266,259.1156107,722.2520142,210.1648471 +-75,0,-10,690.5580243,449.7647516,778.8949701,420.7922837,778.8949701,364.2057163,710.5580243,330.2332484 +-75,5,-10,702.2520142,569.8331529,764.0984266,525.8823893,793.6465635,468.9765706,698.9045851,449.885293 +-75,10,-10,713.9867671,690.3200879,749.2567271,631.2932078,808.3534113,573.429626,687.2914862,569.1231429 +-75,15,-10,725.7624962,811.2277487,734.3696646,737.0262095,823.0157166,677.5663263,675.7185184,687.9489451 +-70,-15,-10,768.5742793,92.44208812,910.4723762,107.1270958,822.261592,45.65130015,859.463859,-28.22784715 +-70,-10,-10,780.2874402,210.711802,895.8825272,211.69468,837.0760238,151.8285016,847.5464538,92.10415539 +-70,-5,-10,792.0410277,329.3897103,881.248172,316.5812461,851.8451261,257.6808199,835.6703227,212.0194073 +-70,0,-10,803.8352515,448.47793,866.5691067,421.7882559,866.5691067,363.2097441,823.8352515,331.52007 +-70,5,-10,815.6703227,567.9785927,851.8451261,527.3171801,881.248172,468.4167539,812.0410277,450.6082897 +-70,10,-10,827.5464538,687.8938446,837.0760238,633.1694984,895.8825272,573.30332,800.2874402,569.286198 +-70,15,-10,839.463859,808.2258471,822.261592,739.3466998,910.4723762,677.8709042,788.5742793,687.5559119 +-65,-15,-10,880.3791896,92.83146159,998.6554522,106.8199881,910.8903011,43.31135758,972.095511,-25.2541877 +-65,-10,-10,892.229434,210.5502678,984.1397731,211.8220368,925.6303607,149.9365065,960.0396072,94.5076118 +-65,-5,-10,904.1203871,328.6734689,969.5796296,317.1457269,940.3251299,256.2340385,948.0252614,213.8565799 +-65,0,-10,916.0522591,447.2031524,954.9748171,422.7925388,954.9748171,362.2054612,936.0522591,332.7948476 +-65,5,-10,928.0252614,566.1414201,940.3251299,528.7639615,969.5796296,467.8522731,924.1203871,451.3245311 +-65,10,-10,940.0396072,685.4903882,925.6303607,635.0614935,984.1397731,573.1759632,912.229434,569.4477322 +-65,15,-10,952.095511,805.2521877,910.8903011,741.6866424,998.6554522,678.1780119,900.3791896,687.1665384 +-60,-15,-10,991.1478587,93.21722624,1087.574033,106.5103189,1000.265095,40.95171714,1083.672477,-22.30837368 +-60,-10,-10,1003.132145,210.3902332,1073.134273,211.9504574,1014.929005,148.0286091,1071.481182,96.88860086 +-60,-5,-10,1015.15741,327.9638885,1058.650096,317.7149304,1029.547669,254.7751155,1059.331715,215.6766082 +-60,0,-10,1027.223862,445.9402505,1044.121296,423.8052367,1044.121296,361.1927633,1047.223862,334.0577495 +-60,5,-10,1039.331715,564.3213918,1029.547669,530.2228845,1058.650096,467.2830696,1035.15741,452.0341115 +-60,10,-10,1051.481182,683.1093991,1014.929005,636.9693909,1073.134273,573.0475426,1023.132145,569.6077668 +-60,15,-10,1063.672477,802.3063737,1000.265095,744.0462829,1087.574033,678.4876811,1011.147859,686.7807738 +-55,-15,-10,1100.894626,93.599432,1177.237358,106.198056,1090.395436,38.57212905,1194.209503,-19.3900158 +-55,-10,-10,1113.009981,210.2316776,1162.875302,212.0799552,1104.98138,146.1046079,1181.885855,99.24743613 +-55,-5,-10,1125.166569,327.2608767,1148.468883,318.288916,1119.52213,253.3038975,1169.604293,217.479731 +-55,0,-10,1137.364602,444.6890592,1134.017894,424.826456,1134.017894,360.171544,1157.364602,335.3089408 +-55,5,-10,1149.604293,562.518269,1119.52213,531.6941025,1148.468883,466.709084,1145.166569,452.7371233 +-55,10,-10,1161.885855,680.7505639,1104.98138,638.8933921,1162.875302,572.9180448,1133.009981,569.7663224 +-55,15,-10,1174.209503,799.3880158,1090.395436,746.425871,1177.237358,678.799944,1120.894626,686.398568 +-50,-15,-10,1209.633569,93.97812789,1267.654824,105.8831668,1181.290943,36.17233929,1303.721059,-16.49873196 +-50,-10,-10,1221.877081,210.0745805,1253.372295,212.2105439,1195.797069,144.1642983,1291.26803,101.5844254 +-50,-5,-10,1234.162071,326.5643426,1239.045462,318.8677442,1210.258057,251.8202283,1278.857332,219.2661829 +-50,0,-10,1246.488751,443.4494163,1224.674119,425.8563046,1224.674119,359.1416954,1266.488751,336.5485837 +-50,5,-10,1258.857332,560.7318171,1210.258057,533.1777717,1239.045462,466.1302558,1254.162071,453.4336574 +-50,10,-10,1271.26803,678.4135746,1195.797069,640.8337017,1253.372295,572.7874561,1241.877081,569.9234195 +-50,15,-10,1283.721059,796.496732,1181.290943,748.8256607,1267.654824,679.1148332,1229.633569,686.0198721 +-45,-15,-10,1317.378506,94.35336204,1358.835986,105.5656179,1272.961404,33.75208952,1412.221351,-13.63414717 +-45,-10,-10,1329.747329,209.9189219,1344.634844,212.3422374,1287.385819,142.2074719,1399.641847,103.8998707 +-45,-5,-10,1342.157862,325.8741972,1330.389464,319.4514766,1301.76516,250.3239493,1387.104908,221.0361939 +-45,0,-10,1354.610317,442.2211626,1316.099639,426.8948923,1316.099639,358.1031077,1374.610317,337.7768374 +-45,5,-10,1367.104908,558.9618061,1301.76516,534.6740507,1330.389464,465.5465234,1362.157862,454.1238028 +-45,10,-10,1379.641847,676.0981293,1287.385819,642.7905281,1344.634844,572.6557626,1349.747329,570.0790781 +-45,15,-10,1392.221351,793.6321472,1272.961404,751.2459105,1358.835986,679.4323821,1337.378506,685.644638 +-40,-15,-10,1424.143004,94.7251817,1450.79056,105.2453755,1365.41677,31.31111696,1519.724321,-10.7958933 +-40,-10,-10,1436.634353,209.7646821,1436.672704,212.4750496,1379.757542,140.233917,1507.021186,106.1940686 +-40,-5,-10,1449.167633,325.1903529,1422.510683,320.0401758,1394.053311,248.814899,1494.360836,222.7899899 +-40,0,-10,1461.743057,441.004142,1408.304288,427.9423308,1408.304288,357.0556692,1481.743057,338.993858 +-40,5,-10,1474.360836,557.2080101,1394.053311,536.183101,1422.510683,464.9578242,1469.167633,454.8076471 +-40,10,-10,1487.021186,673.8039314,1379.757542,644.764083,1436.672704,572.5229504,1456.634353,570.2333179 +-40,15,-10,1499.724321,790.7938933,1365.41677,753.686883,1450.79056,679.7526245,1444.143004,685.2728183 +-35,-15,-10,1529.940387,95.09363327,1543.528428,104.9224052,1458.667166,28.8491543,1626.243658,-7.983608956 +-35,-10,-10,1542.551536,209.6118417,1529.495798,212.6089949,1472.922323,138.2434182,1613.419671,108.4673103 +-35,-5,-10,1555.204827,324.5127239,1515.419081,320.6339055,1487.132553,247.2929131,1600.638681,224.5277928 +-35,0,-10,1567.900471,439.7982009,1501.298069,428.9987339,1501.298069,355.9992661,1587.900471,340.1997991 +-35,5,-10,1580.638681,555.4702072,1487.132553,537.7050869,1515.419081,464.3640945,1575.204827,455.4852761 +-35,10,-10,1593.419671,671.5306897,1472.922323,646.7545818,1529.495798,572.3890051,1562.551536,570.3861583 +-35,15,-10,1606.243658,787.981609,1458.667166,756.1488457,1543.528428,680.0755948,1549.940387,684.9043667 +-30,-15,-10,1634.783734,95.4587623,1637.059641,104.596672,1552.722891,26.36592965,1731.792799,-5.196939348 +-30,-10,-10,1647.512018,209.460382,1623.114216,212.7440879,1566.890418,136.2357563,1718.850683,110.7198815 +-30,-5,-10,1660.282643,323.8412257,1609.124791,321.2327304,1581.013102,245.7578246,1705.951762,226.2498204 +-30,0,-10,1673.09582,438.6031889,1595.091156,430.064217,1595.091156,354.933783,1693.09582,341.3948111 +-30,5,-10,1685.951762,553.7481796,1581.013102,539.2401754,1609.124791,463.7652696,1680.282643,456.1567743 +-30,10,-10,1698.850683,669.2781185,1566.890418,648.7622437,1623.114216,572.2539121,1667.512018,570.537618 +-30,15,-10,1711.792799,785.1949393,1552.722891,758.6320704,1637.059641,680.401328,1654.783734,684.5392377 +-25,-15,-10,1738.685893,95.82061353,1731.394425,104.2681402,1647.594423,23.86116636,1836.384941,-2.435536105 +-25,-10,-10,1751.528703,209.3102841,1717.538226,212.8803434,1661.672262,134.2107084,1823.327356,112.9520629 +-25,-5,-10,1764.414042,323.1757756,1703.638121,321.8367164,1675.705349,244.2094637,1810.313155,227.9562865 +-25,0,-10,1777.342122,437.418958,1689.693898,431.1388977,1689.693898,353.8591023,1797.342122,342.579042 +-25,5,-10,1790.313155,552.0417135,1675.705349,540.7885363,1703.638121,463.1612836,1784.414042,456.8222244 +-25,10,-10,1803.327356,667.0459371,1661.672262,650.7872916,1717.538226,572.1176566,1771.528703,570.6877159 +-25,15,-10,1816.384941,782.4335361,1647.594423,761.1368336,1731.394425,680.7298598,1758.685893,684.1773865 +-20,-15,-10,1841.659481,96.17923091,1826.543178,103.9367738,1743.292421,21.334583,1940.033039,0.300942854 +-20,-10,-10,1854.614264,209.1615299,1812.778271,213.0177764,1757.278471,132.1680475,1926.86259,115.1641302 +-20,-5,-10,1867.611754,322.5162922,1798.969556,322.4459304,1771.219868,242.6476573,1913.735703,229.647401 +-20,0,-10,1880.652162,436.2453629,1785.116826,432.2228957,1785.116826,352.7751043,1900.652162,343.7526371 +-20,5,-10,1893.735703,550.350599,1771.219868,542.3503427,1798.969556,462.5520696,1887.611754,457.4817078 +-20,10,-10,1906.86259,664.8338698,1757.278471,652.8299525,1812.778271,571.9802236,1874.614264,570.8364701 +-20,15,-10,1920.033039,779.6970571,1743.292421,763.663417,1826.543178,681.0612262,1861.659481,683.8187691 +-15,-15,-10,1943.716891,96.5346576,1922.516481,103.6025357,1839.827734,18.78589319,2042.749818,3.01283346 +-15,-10,-10,1956.781147,209.0141013,1908.844973,213.1564023,1853.719848,130.1075431,2029.469053,117.3563539 +-15,-5,-10,1969.888279,321.8626957,1895.129767,323.0604408,1867.567416,241.0722297,2016.232019,231.3233702 +-15,0,-10,1983.038499,435.0822611,1881.370651,433.3163326,1881.370651,351.6816674,2003.038499,344.9157389 +-15,5,-10,1996.232019,548.6746298,1867.567416,543.9257703,1895.129767,461.9375592,1989.888279,458.1353043 +-15,10,-10,2009.469053,662.6416461,1853.719848,654.8904569,1908.844973,571.8415977,1976.781147,570.9838987 +-15,15,-10,2022.749818,776.9851665,1839.827734,766.2121068,1922.516481,681.3954643,1963.716891,683.4633424 +-10,-15,-10,2044.870295,96.88693597,2019.325101,103.2653885,1937.211398,16.21480551,2144.547773,5.700465635 +-10,-10,-10,2058.041579,208.8679807,2005.749145,213.2962367,1951.007384,128.0289601,2131.159186,119.529 +-10,-5,-10,2071.255898,321.2149075,1992.129606,323.6803167,1964.758938,239.4830019,2117.814488,232.9843967 +-10,0,-10,2084.513464,433.9295124,1978.466274,434.4193323,1978.466274,350.5786677,2104.513464,346.0684876 +-10,5,-10,2097.814488,547.0136033,1964.758938,545.5149981,1992.129606,461.3176833,2091.255898,458.7830925 +-10,10,-10,2111.159186,660.469,1951.007384,656.9690399,2005.749145,571.7017633,2078.041579,571.1300193 +-10,15,-10,2124.547773,774.2975344,1937.211398,768.7831945,2019.325101,681.7326115,2064.870295,683.111064 +-5,-15,-10,2145.131652,97.23610769,2116.979989,102.9252941,2035.454645,13.62102343,2245.439179,8.364163426 +-5,-10,-10,2158.40757,208.7231509,2103.501783,213.4372954,2049.152263,125.9320597,2231.945211,121.6823294 +-5,-5,-10,2171.726673,320.5728506,2089.98012,324.3056289,2062.805572,237.8797917,2218.49528,234.6306794 +-5,0,-10,2185.089172,432.7869791,2076.414787,435.5320208,2076.414787,349.4659792,2205.089172,347.2110209 +-5,5,-10,2198.49528,545.3673206,2062.805572,547.1182083,2089.98012,460.6923711,2191.726673,459.4251494 +-5,10,-10,2211.945211,658.3156706,2049.152263,659.0659403,2103.501783,571.5607046,2178.40757,571.2748491 +-5,15,-10,2225.439179,771.6338366,2035.454645,771.3769766,2116.979989,682.0727059,2165.131652,682.7618923 +0,-15,-10,2244.51271,97.58221367,2215.49229,102.5822137,2134.568908,11.00424513,2345.436092,11.00424513 +0,-10,-10,2257.890918,208.5795947,2202.114082,213.5795947,2148.16587,123.8165986,2331.83913,123.8165986 +0,-5,-10,2271.312453,319.9364492,2188.692547,324.9364492,2161.718654,236.2624137,2318.286346,236.2624137 +0,0,-10,2284.777525,431.6545262,2175.227475,436.6545262,2175.227475,348.3434738,2304.777525,348.3434738 +0,5,-10,2298.286346,543.7355863,2161.718654,548.7355863,2188.692547,460.0615508,2291.312453,460.0615508 +0,10,-10,2311.83913,656.1814014,2148.16587,661.1814014,2202.114082,571.4184053,2277.890918,571.4184053 +0,15,-10,2325.436092,768.9937549,2134.568908,773.9937549,2215.49229,682.4157863,2264.51271,682.4157863 +5,-15,-10,2343.025011,97.92529411,2314.873348,102.2361077,2234.565821,8.364163426,2444.550355,13.62102343 +5,-10,-10,2356.503217,208.4372954,2301.59743,213.7231509,2248.059789,121.6823294,2430.852737,125.9320597 +5,-5,-10,2370.02488,319.3056289,2288.278327,325.5728506,2261.50972,234.6306794,2417.199428,237.8797917 +5,0,-10,2383.590213,430.5320208,2274.915828,437.7869791,2274.915828,347.2110209,2403.590213,349.4659792 +5,5,-10,2397.199428,542.1182083,2261.50972,550.3673206,2288.278327,459.4251494,2390.02488,460.6923711 +5,10,-10,2410.852737,654.0659403,2248.059789,663.3156706,2301.59743,571.2748491,2376.503217,571.5607046 +5,15,-10,2424.550355,766.3769766,2234.565821,776.6338366,2314.873348,682.7618923,2363.025011,682.0727059 +10,-15,-10,2440.679899,98.2653885,2415.134705,101.886936,2335.457227,5.700465635,2542.793602,16.21480551 +10,-10,-10,2454.255855,208.2962367,2401.963421,213.8679807,2348.845814,119.529,2528.997616,128.0289601 +10,-5,-10,2467.875394,318.6803167,2388.749102,326.2149075,2362.190512,232.9843967,2515.246062,239.4830019 +10,0,-10,2481.538726,429.4193323,2375.491536,438.9295124,2375.491536,346.0684876,2501.538726,350.5786677 +10,5,-10,2495.246062,540.5149981,2362.190512,552.0136033,2388.749102,458.7830925,2487.875394,461.3176833 +10,10,-10,2508.997616,651.9690399,2348.845814,665.469,2401.963421,571.1300193,2474.255855,571.7017633 +10,15,-10,2522.793602,763.7831945,2335.457227,779.2975344,2415.134705,683.111064,2460.679899,681.7326115 +15,-15,-10,2537.488519,98.60253567,2516.288109,101.5346576,2437.255182,3.01283346,2640.177266,18.78589319 +15,-10,-10,2551.160027,208.1564023,2503.223853,214.0141013,2450.535947,117.3563539,2626.285152,130.1075431 +15,-5,-10,2564.875233,318.0604408,2490.116721,326.8626957,2463.772981,231.3233702,2612.437584,241.0722297 +15,0,-10,2578.634349,428.3163326,2476.966501,440.0822611,2476.966501,344.9157389,2598.634349,351.6816674 +15,5,-10,2592.437584,538.9257703,2463.772981,553.6746298,2490.116721,458.1353043,2584.875233,461.9375592 +15,10,-10,2606.285152,649.8904569,2450.535947,667.6416461,2503.223853,570.9838987,2571.160027,571.8415977 +15,15,-10,2620.177266,761.2121068,2437.255182,781.9851665,2516.288109,683.4633424,2557.488519,681.3954643 +20,-15,-10,2633.461822,98.93677375,2618.345519,101.1792309,2539.971961,0.300942854,2736.712579,21.334583 +20,-10,-10,2647.226729,208.0177764,2605.390736,214.1615299,2553.14241,115.1641302,2722.726529,132.1680475 +20,-5,-10,2661.035444,317.4459304,2592.393246,327.5162922,2566.269297,229.647401,2708.785132,242.6476573 +20,0,-10,2674.888174,427.2228957,2579.352838,441.2453629,2579.352838,343.7526371,2694.888174,352.7751043 +20,5,-10,2688.785132,537.3503427,2566.269297,555.350599,2592.393246,457.4817078,2681.035444,462.5520696 +20,10,-10,2702.726529,647.8299525,2553.14241,669.8338698,2605.390736,570.8364701,2667.226729,571.9802236 +20,15,-10,2716.712579,758.663417,2539.971961,784.6970571,2618.345519,683.8187691,2653.461822,681.0612262 +25,-15,-10,2728.610575,99.26814024,2721.319107,100.8206135,2643.620059,-2.435536105,2832.410577,23.86116636 +25,-10,-10,2742.466774,207.8803434,2708.476297,214.3102841,2656.677644,112.9520629,2818.332738,134.2107084 +25,-5,-10,2756.366879,316.8367164,2695.590958,328.1757756,2669.691845,227.9562865,2804.299651,244.2094637 +25,0,-10,2770.311102,426.1388977,2682.662878,442.418958,2682.662878,342.579042,2790.311102,353.8591023 +25,5,-10,2784.299651,535.7885363,2669.691845,557.0417135,2695.590958,456.8222244,2776.366879,463.1612836 +25,10,-10,2798.332738,645.7872916,2656.677644,672.0459371,2708.476297,570.6877159,2762.466774,572.1176566 +25,15,-10,2812.410577,756.1368336,2643.620059,787.4335361,2721.319107,684.1773865,2748.610575,680.7298598 +30,-15,-10,2822.945359,99.59667199,2825.221266,100.4587623,2748.212201,-5.196939348,2927.282109,26.36592965 +30,-10,-10,2836.890784,207.7440879,2812.492982,214.460382,2761.154317,110.7198815,2913.114582,136.2357563 +30,-5,-10,2850.880209,316.2327304,2799.722357,328.8412257,2774.053238,226.2498204,2898.991898,245.7578246 +30,0,-10,2864.913844,425.064217,2786.90918,443.6031889,2786.90918,341.3948111,2884.913844,354.933783 +30,5,-10,2878.991898,534.2401754,2774.053238,558.7481796,2799.722357,456.1567743,2870.880209,463.7652696 +30,10,-10,2893.114582,643.7622437,2761.154317,674.2781185,2812.492982,570.537618,2856.890784,572.2539121 +30,15,-10,2907.282109,753.6320704,2748.212201,790.1949393,2825.221266,684.5392377,2842.945359,680.401328 +35,-15,-10,2916.476572,99.92240521,2930.064613,100.0936333,2853.761342,-7.983608956,3021.337834,28.8491543 +35,-10,-10,2930.509202,207.6089949,2917.453464,214.6118417,2866.585329,108.4673103,3007.082677,138.2434182 +35,-5,-10,2944.585919,315.6339055,2904.800173,329.5127239,2879.366319,224.5277928,2992.872447,247.2929131 +35,0,-10,2958.706931,423.9987339,2892.104529,444.7982009,2892.104529,340.1997991,2978.706931,355.9992661 +35,5,-10,2972.872447,532.7050869,2879.366319,560.4702072,2904.800173,455.4852761,2964.585919,464.3640945 +35,10,-10,2987.082677,641.7545818,2866.585329,676.5306897,2917.453464,570.3861583,2950.509202,572.3890051 +35,15,-10,3001.337834,751.1488457,2853.761342,792.981609,2930.064613,684.9043667,2936.476572,680.0755948 +40,-15,-10,3009.21444,100.2453755,3035.861996,99.7251817,2960.280679,-10.7958933,3114.58823,31.31111696 +40,-10,-10,3023.332296,207.4750496,3023.370647,214.7646821,2972.983814,106.1940686,3100.247458,140.233917 +40,-5,-10,3037.494317,315.0401758,3010.837367,330.1903529,2985.644164,222.7899899,3085.951689,248.814899 +40,0,-10,3051.700712,422.9423308,2998.261943,446.004142,2998.261943,338.993858,3071.700712,357.0556692 +40,5,-10,3065.951689,531.183101,2985.644164,562.2080101,3010.837367,454.8076471,3057.494317,464.9578242 +40,10,-10,3080.247458,639.764083,2972.983814,678.8039314,3023.370647,570.2333179,3043.332296,572.5229504 +40,15,-10,3094.58823,748.686883,2960.280679,795.7938933,3035.861996,685.2728183,3029.21444,679.7526245 +45,-15,-10,3101.169014,100.5656179,3142.626494,99.35336204,3067.783649,-13.63414717,3207.043596,33.75208952 +45,-10,-10,3115.370156,207.3422374,3130.257671,214.9189219,3080.363153,103.8998707,3192.619181,142.2074719 +45,-5,-10,3129.615536,314.4514766,3117.847138,330.8741972,3092.900092,221.0361939,3178.23984,250.3239493 +45,0,-10,3143.905361,421.8948923,3105.394683,447.2211626,3105.394683,337.7768374,3163.905361,358.1031077 +45,5,-10,3158.23984,529.6740507,3092.900092,563.9618061,3117.847138,454.1238028,3149.615536,465.5465234 +45,10,-10,3172.619181,637.7905281,3080.363153,681.0981293,3130.257671,570.0790781,3135.370156,572.6557626 +45,15,-10,3187.043596,746.2459105,3067.783649,798.6321472,3142.626494,685.644638,3121.169014,679.4323821 +50,-15,-10,3192.350176,100.8831668,3250.371431,98.97812789,3176.283941,-16.49873196,3298.714057,36.17233929 +50,-10,-10,3206.632705,207.2105439,3238.127919,215.0745805,3188.73697,101.5844254,3284.207931,144.1642983 +50,-5,-10,3220.959538,313.8677442,3225.842929,331.5643426,3201.147668,219.2661829,3269.746943,251.8202283 +50,0,-10,3235.330881,420.8563046,3213.516249,448.4494163,3213.516249,336.5485837,3255.330881,359.1416954 +50,5,-10,3249.746943,528.1777717,3201.147668,565.7318171,3225.842929,453.4336574,3240.959538,466.1302558 +50,10,-10,3264.207931,635.8337017,3188.73697,683.4135746,3238.127919,569.9234195,3226.632705,572.7874561 +50,15,-10,3278.714057,743.8256607,3176.283941,801.496732,3250.371431,686.0198721,3212.350176,679.1148332 +55,-15,-10,3282.767642,101.198056,3359.110374,98.599432,3285.795497,-19.3900158,3389.609564,38.57212905 +55,-10,-10,3297.129698,207.0799552,3346.995019,215.2316776,3298.119145,99.24743613,3375.02362,146.1046079 +55,-5,-10,3311.536117,313.288916,3334.838431,332.2608767,3310.400707,217.479731,3360.48287,253.3038975 +55,0,-10,3325.987106,419.826456,3322.640398,449.6890592,3322.640398,335.3089408,3345.987106,360.171544 +55,5,-10,3340.48287,526.6941025,3310.400707,567.518269,3334.838431,452.7371233,3331.536117,466.709084 +55,10,-10,3355.02362,633.8933921,3298.119145,685.7505639,3346.995019,569.7663224,3317.129698,572.9180448 +55,15,-10,3369.609564,741.425871,3285.795497,804.3880158,3359.110374,686.398568,3302.767642,678.799944 +60,-15,-10,3372.430967,101.5103189,3468.857141,98.21722624,3396.332523,-22.30837368,3479.739905,40.95171714 +60,-10,-10,3386.870727,206.9504574,3456.872855,215.3902332,3408.523818,96.88860086,3465.075995,148.0286091 +60,-5,-10,3401.354904,312.7149304,3444.84759,332.9638885,3420.673285,215.6766082,3450.457331,254.7751155 +60,0,-10,3415.883704,418.8052367,3432.781138,450.9402505,3432.781138,334.0577495,3435.883704,361.1927633 +60,5,-10,3430.457331,525.2228845,3420.673285,569.3213918,3444.84759,452.0341115,3421.354904,467.2830696 +60,10,-10,3445.075995,631.9693909,3408.523818,688.1093991,3456.872855,569.6077668,3406.870727,573.0475426 +60,15,-10,3459.739905,739.0462829,3396.332523,807.3063737,3468.857141,686.7807738,3392.430967,678.4876811 +65,-15,-10,3461.349548,101.8199881,3579.62581,97.83146159,3507.909489,-25.2541877,3569.114699,43.31135758 +65,-10,-10,3475.865227,206.8220368,3567.775566,215.5502678,3519.965393,94.5076118,3554.374639,149.9365065 +65,-5,-10,3490.42537,312.1457269,3555.884613,333.6734689,3531.979739,213.8565799,3539.67987,256.2340385 +65,0,-10,3505.030183,417.7925388,3543.952741,452.2031524,3543.952741,332.7948476,3525.030183,362.2054612 +65,5,-10,3519.67987,523.7639615,3531.979739,571.1414201,3555.884613,451.3245311,3510.42537,467.8522731 +65,10,-10,3534.374639,630.0614935,3519.965393,690.4903882,3567.775566,569.4477322,3495.865227,573.1759632 +65,15,-10,3549.114699,736.6866424,3507.909489,810.2521877,3579.62581,687.1665384,3481.349548,678.1780119 +70,-15,-10,3549.532624,102.1270958,3691.430721,97.44208812,3620.541141,-28.22784715,3657.743408,45.65130015 +70,-10,-10,3564.122473,206.69468,3679.71756,215.711802,3632.458546,92.10415539,3642.928976,151.8285016 +70,-5,-10,3578.756828,311.5812461,3667.963972,334.3897103,3644.334677,212.0194073,3628.159874,257.6808199 +70,0,-10,3593.435893,416.7882559,3656.169749,453.47793,3656.169749,331.52007,3613.435893,363.2097441 +70,5,-10,3608.159874,522.3171801,3644.334677,572.9785927,3667.963972,450.6082897,3598.756828,468.4167539 +70,10,-10,3622.928976,628.1694984,3632.458546,692.8938446,3679.71756,569.286198,3584.122473,573.30332 +70,15,-10,3637.743408,734.3466998,3620.541141,813.2258471,3691.430721,687.5559119,3569.532624,677.8709042 +75,-15,-10,3636.989283,102.4316737,3804.286482,97.04905494,3734.242504,-31.2297487,3745.635335,47.97179049 +75,-10,-10,3651.651589,206.568374,3792.713514,215.8748571,3746.018233,89.67791212,3730.748273,153.7047922 +75,-5,-10,3666.358436,311.0214294,3781.100415,335.112707,3757.752986,210.1648471,3715.906573,259.1156107 +75,0,-10,3681.11003,415.7922837,3769.446976,454.7647516,3769.446976,330.2332484,3701.11003,364.2057163 +75,5,-10,3695.906573,520.8823893,3757.752986,574.8331529,3781.100415,449.885293,3686.358436,468.9765706 +75,10,-10,3710.748273,626.2932078,3746.018233,695.3200879,3792.713514,569.1231429,3671.651589,573.429626 +75,15,-10,3725.635335,732.0262095,3734.242504,816.2277487,3804.286482,687.9489451,3656.989283,677.5663263 +80,-15,-10,3723.728466,102.7337529,3918.207979,96.6523102,3849.028889,-34.26029663,3832.79963,50.27307014 +80,-10,-10,3738.461548,206.4431058,3906.778383,216.0394547,3860.659693,87.22855638,3817.841643,155.5655732 +80,-5,-10,3753.239204,310.4662192,3895.308966,335.842555,3872.249832,208.2926511,3802.929047,260.5385593 +80,0,-10,3768.061636,414.8045193,3883.799519,456.0637888,3883.799519,328.9342112,3788.061636,365.1934807 +80,5,-10,3782.929047,519.4594407,3872.249832,576.7053489,3895.308966,449.155445,3773.239204,469.5317808 +80,10,-10,3797.841643,624.4324268,3860.659693,697.7694436,3906.778383,568.9585453,3758.461548,573.5548942 +80,15,-10,3812.79963,729.7249299,3849.028889,819.2582966,3918.207979,688.3456898,3743.728466,677.2642471 +85,-15,-10,3809.758964,103.033364,4033.210379,96.2518011,3964.915904,-37.31990295,3919.245292,52.55537669 +85,-10,-10,3824.561179,206.3188625,4021.927408,216.2056167,3976.398457,84.75575633,3904.218051,157.411036 +85,-5,-10,3839.407992,309.9155589,4010.604939,336.5793521,3987.840673,206.4025666,3889.236225,261.9498117 +85,0,-10,3854.299607,413.8248618,3999.242764,457.3752162,3999.242764,327.6227838,3874.299607,366.1731382 +85,5,-10,3869.236225,518.0481883,3987.840673,578.5954334,4010.604939,448.4186479,3859.407992,470.0824411 +85,10,-10,3884.218051,622.586964,3976.398457,700.2422437,4021.927408,568.7923833,3844.561179,573.6791375 +85,15,-10,3899.245292,727.4426233,3964.915904,822.3179029,4033.210379,688.7461989,3829.758964,676.964636 +90,-15,-10,3895.089427,103.3305372,4149.309141,95.84747379,4081.919452,-40.40898761,4004.98117,54.81894381 +90,-10,-10,3909.959162,206.1956317,4138.176118,216.3733656,4093.250357,82.25917375,3989.886314,159.241369 +90,-5,-10,3924.873517,309.3693928,4127.003937,337.3231981,4104.541265,204.4943361,3974.83689,263.3495116 +90,0,-10,3939.832692,412.8532118,4115.792388,458.699212,4115.792388,326.298788,3959.832692,367.1447882 +90,5,-10,3954.83689,516.6484884,4104.541265,580.5036639,4127.003937,447.6748019,3944.873517,470.6286072 +90,10,-10,3969.886314,620.756631,4093.250357,702.7388262,4138.176118,568.6246344,3929.959162,573.8023683 +90,15,-10,3984.98117,725.1790562,4081.919452,825.4069876,4149.309141,689.1505262,3915.089427,676.6674628 +95,-15,-10,3979.728366,103.625302,4266.520016,95.43927342,4200.05575,-43.52797869,4090.015971,57.06400136 +95,-10,-10,3994.664041,206.0734011,4255.540343,216.5427242,4211.231527,79.7384639,4074.855104,161.0567576 +95,-5,-10,4009.644352,308.8276661,4244.521865,338.0741946,4222.367666,202.5676969,4059.739681,264.7378002 +95,0,-10,4024.669498,411.8894714,4233.464375,460.0359578,4233.464375,324.9620422,4044.669498,368.1085286 +95,5,-10,4039.739681,515.2601998,4222.367666,582.4303031,4244.521865,446.9238054,4029.644352,471.1703339 +95,10,-10,4054.855104,618.9412424,4211.231527,705.2595361,4255.540343,568.4552758,4014.664041,573.9245989 +95,15,-10,4070.015971,722.9339986,4200.05575,828.5259787,4266.520016,689.5587266,3999.728366,676.372698 +100,-15,-10,4063.684153,103.9176877,4384.859061,95.02714407,4319.341327,-46.67731261,4174.35826,59.29077543 +100,-10,-10,4078.68422,205.9521586,4374.036217,216.7137158,4330.358419,77.19327531,4159.132953,162.8573839 +100,-5,-10,4093.728933,308.2903249,4363.174936,338.8324452,4341.336245,200.6223813,4143.953099,266.1148165 +100,0,-10,4108.818493,410.9335445,4352.275015,461.3856386,4352.275015,323.6123614,4128.818493,369.0644555 +100,5,-10,4123.953099,513.8831835,4341.336245,584.3756187,4363.174936,446.1655548,4113.728933,471.7076751 +100,10,-10,4139.132953,617.1406161,4330.358419,707.8047247,4374.036217,568.2842842,4098.68422,574.0458414 +100,15,-10,4154.35826,720.7072246,4319.341327,831.6753126,4384.859061,689.9708559,4083.684153,676.0803123 +105,-15,-10,4146.965025,104.2077229,4504.342644,94.61102873,4439.793036,-49.85743431,4258.016461,61.49948848 +105,-10,-10,4162.027967,205.8318921,4493.680186,216.8863641,4450.647801,74.62324968,4242.728255,164.6434274 +105,-5,-10,4177.135561,307.7573161,4482.979677,339.5980555,4461.463691,198.6581165,4227.485505,267.4806972 +105,0,-10,4192.288007,409.9853365,4472.240914,462.7484431,4472.240914,322.2495569,4212.288007,370.0126635 +105,5,-10,4207.485505,512.5173028,4461.463691,586.3398835,4482.979677,445.3999445,4197.135561,472.2406839 +105,10,-10,4222.728255,615.3545726,4450.647801,710.3747503,4493.680186,568.1116359,4182.027967,574.1661079 +105,15,-10,4238.016461,718.4985115,4439.793036,834.8554343,4504.342644,690.3869713,4166.965025,675.7902771 +-105,-15,-5,3.499862592,59.2479239,250.9323411,84.07579579,158.8315274,83.54653435,88.98845213,-23.97901238 +-105,-10,-5,14.31679741,182.2569254,235.6964047,186.2062336,174.296546,187.2125698,77.97515054,101.2630505 +-105,-5,-5,25.17258317,305.7077355,220.4150609,288.6410492,189.7153367,290.5687275,67.00154777,226.0536629 +-105,0,-5,36.06742956,429.6027388,205.0881064,391.3816052,205.0881064,393.6163948,56.06742956,350.3952612 +-105,5,-5,47.00154777,553.9443371,189.7153367,494.4292725,220.4150609,496.3569508,45.17258317,474.2902645 +-105,10,-5,57.97515054,678.7349495,174.296546,597.7854302,235.6964047,598.7917664,34.31679741,597.7410746 +-105,15,-5,68.98845213,803.9770124,158.8315274,701.4514657,250.9323411,700.9222042,23.49986259,720.7500761 +-100,-15,-5,123.5167508,59.81376347,334.407896,83.68223641,242.6839157,81.41612799,209.9808945,-20.90500251 +-100,-10,-5,134.4953024,182.2269323,319.2347601,186.2270824,258.0861264,185.5091335,198.8040214,103.7194929 +-100,-5,-5,145.5130938,305.0776327,304.0162199,289.0787848,273.442109,289.289715,187.6672402,227.8969553 +-100,0,-5,156.5703356,428.3682145,288.7520713,392.2387231,288.7520713,392.7592769,176.5703356,351.6297855 +-100,5,-5,167.6672402,552.1010447,273.442109,495.708285,304.0162199,495.9192152,165.5130938,474.9203673 +-100,10,-5,178.8040214,676.2785071,258.0861264,599.4888665,319.2347601,598.7709176,154.4953024,597.7710677 +-100,15,-5,189.9808945,800.9030025,242.6839157,703.581872,334.407896,701.3157636,143.5167508,720.1842365 +-95,-15,-5,242.3766682,60.3741483,418.5635864,83.28547042,327.2257054,79.26820626,329.7944262,-17.86094479 +-95,-10,-5,253.5132218,182.1972289,403.4547837,186.2481013,342.5635522,183.7917134,318.4576805,106.1520434 +-95,-5,-5,264.6893873,304.4536257,388.3005844,289.5200977,357.8551754,288.0002187,307.1614027,229.7223514 +-95,0,-5,275.9053765,427.1456547,373.1007836,393.1028561,373.1007836,391.8951439,295.9053765,352.8523453 +-95,5,-5,287.1614027,550.2756486,357.8551754,496.9977813,388.3005844,495.4779023,284.6893873,475.5443743 +-95,10,-5,298.4576805,673.8459566,342.5635522,601.2062866,403.4547837,598.7498987,273.5132218,597.8007711 +-95,15,-5,309.7944262,797.8589448,327.2257054,705.7297937,418.5635864,701.7125296,262.3766682,719.6238517 +-90,-15,-5,360.0962643,60.9291569,503.4077587,82.88545847,412.4654335,77.10255227,448.4461941,-14.84640356 +-90,-10,-5,371.3872867,182.1678109,488.3648531,186.2692924,427.7373283,182.0601366,436.9531903,108.5610487 +-90,-5,-5,382.7182771,303.8356265,473.2765636,289.9650319,442.9630088,286.7001091,425.5010141,231.5301105 +-90,0,-5,394.0894482,425.9348864,458.1426845,393.9740906,458.1426845,391.0239094,414.0894482,354.0631136 +-90,5,-5,405.5010141,548.4678895,442.9630088,498.2978909,473.2765636,495.0329681,402.7182771,476.1623735 +-90,10,-5,416.9531903,671.4369513,427.7373283,602.9378634,488.3648531,598.7287076,391.3872867,597.8301891 +-90,15,-5,428.4461941,794.8444036,412.4654335,707.8954477,503.4077587,702.1125415,380.0962643,719.0688431 +-85,-15,-5,476.6918708,61.47886627,588.9488962,82.48216056,498.4117788,74.91894552,565.9530141,-11.8609516 +-85,-10,-5,488.1339082,182.1386744,573.9734842,186.2906578,513.6161004,180.3142272,554.3072849,110.9468492 +-85,-5,-5,499.6162542,303.2235488,558.9527056,290.4136321,528.7742221,285.3892547,542.7027269,233.3204871 +-85,0,-5,511.1391221,424.7357396,543.8863542,394.8525146,543.8863542,390.1454854,531.1391221,355.2622604 +-85,5,-5,522.7027269,546.6775129,528.7742221,499.6087453,558.9527056,494.5843679,519.6162542,476.7744512 +-85,10,-5,534.3072849,669.0511508,513.6161004,604.6837728,573.9734842,598.7073422,508.1339082,597.8593256 +-85,15,-5,545.9530141,791.8589516,498.4117788,710.0790545,588.9488962,702.5158394,496.6918708,718.5191337 +-80,-15,-5,592.1795092,62.02335193,675.1956225,82.07553604,585.0735644,72.71716185,682.3313792,-8.904169876 +-80,-10,-5,603.7691851,182.1098152,660.289333,186.3121997,600.208658,178.5538067,670.5363773,113.3097784 +-80,-5,-5,615.3994951,302.6173078,645.3376997,290.8659438,615.2975713,284.0675216,658.7828749,235.0937306 +-80,0,-5,627.0706531,423.548048,630.3405153,395.7382174,630.3405153,389.2597826,647.0706531,356.449952 +-80,5,-5,638.7828749,544.9042694,615.2975713,500.9304784,645.3376997,494.1320562,635.3994951,477.3806922 +-80,10,-5,650.5363773,666.6882216,600.208658,606.4441933,660.289333,598.6858003,623.7691851,597.8881848 +-80,15,-5,662.3313792,788.9021699,585.0735644,712.2808381,675.1956225,702.922464,612.1795092,717.9746481 +-75,-15,-5,706.5748976,62.562688,762.1567037,81.66554357,672.4597607,70.49697334,797.597467,-5.975647363 +-75,-10,-5,718.308911,182.0812294,747.3211996,186.3339203,687.5239371,176.7786932,785.6565674,115.6501637 +-75,-5,-5,730.0838693,302.0168205,732.4403794,291.3220134,702.5419581,282.734774,773.7574805,236.8500858 +-75,0,-5,741.8999871,422.3716481,717.5140353,396.6312899,717.5140353,388.3667101,761.8999871,357.6263519 +-75,5,-5,753.7574805,543.1479142,702.5419581,502.263226,732.4403794,493.6759866,750.0838693,477.9811795 +-75,10,-5,765.6565674,664.3478363,687.5239371,608.2193068,747.3211996,598.6640797,738.308911,597.9167706 +-75,15,-5,777.597467,785.9736474,672.4597607,714.5010267,762.1567037,703.3324564,726.5748976,717.435312 +-70,-15,-5,819.8934585,63.09694719,849.8410519,81.25214114,760.5794891,68.25814826,911.7671475,-3.074980863 +-70,-10,-5,831.7685817,182.0529131,835.0780303,186.3558219,775.5710238,174.9887022,899.6836494,117.9683262 +-70,-5,-5,843.6849464,301.4220053,820.269726,291.7818878,790.5164333,281.3908735,887.6422628,238.5897928 +-70,0,-5,855.6427679,421.2063797,805.4159303,397.5318244,805.4159303,387.4661756,875.6427679,358.7916203 +-70,5,-5,867.6422628,541.4082072,790.5164333,503.6071265,820.269726,493.2161122,863.6849464,478.5759947 +-70,10,-5,879.6836494,662.0296738,775.5710238,610.0092978,835.0780303,598.6421781,851.7685817,597.9450869 +-70,15,-5,891.7671475,783.0729809,760.5794891,716.7398517,849.8410519,703.7458589,839.8934585,716.9010528 +-65,-15,-5,932.1503252,63.62620084,938.2577281,80.83528602,849.4420245,66.00045096,1024.85599,-0.201774808 +-65,-10,-5,944.1634021,182.0248626,923.5689215,186.3779067,864.3591568,173.1836457,1012.633119,120.264581 +-65,-5,-5,956.2180035,300.8327823,908.834871,292.2456148,879.2301999,280.0356796,1000.452644,240.3130873 +-65,0,-5,968.314345,420.0520856,894.0553674,398.4399149,894.0553674,386.5580851,988.314345,359.9459144 +-65,5,-5,980.4526439,539.6849127,879.2301999,504.9623204,908.834871,492.7523852,976.2180035,479.1652177 +-65,10,-5,992.6331189,659.733419,864.3591568,611.8143543,923.5689215,598.6200933,964.1634021,597.9731374 +-65,15,-5,1004.85599,780.1997748,849.4420245,718.997549,938.2577281,704.162714,952.1503252,716.3717992 +-60,-15,-5,1043.360349,64.15051898,1027.415945,80.41493479,939.056799,63.7236418,1136.879271,2.644358914 +-60,-10,-5,1055.508293,181.9970742,1012.803122,186.4001769,953.8977311,171.3633328,1124.52018,122.539237 +-60,-5,-5,1067.698031,300.249073,998.1450996,292.7132431,968.6926162,278.6690493,1112.203757,242.0202003 +-60,0,-5,1079.92978,418.9086113,983.4416681,399.3556568,983.4416681,385.6423432,1099.92978,361.0893887 +-60,5,-5,1092.203757,537.9777997,968.6926162,506.3289507,998.1450996,492.2847569,1087.698031,479.748927 +-60,10,-5,1104.52018,657.458763,953.8977311,613.6346672,1012.803122,598.5978231,1075.508293,598.0009258 +-60,15,-5,1116.879271,777.3536411,939.056799,721.2743582,1027.415945,704.5830652,1063.360349,715.847481 +-55,-15,-5,1153.538105,64.66997033,1117.325071,79.99104327,1029.433405,61.42747708,1247.851979,5.463801135 +-55,-10,-5,1165.817898,181.9695441,1102.790037,186.422635,1044.196301,169.5275692,1235.359752,124.7925977 +-55,-5,-5,1178.139742,299.6708003,1088.209854,293.1848221,1058.913199,277.2908372,1222.91045,243.7113588 +-55,0,-5,1190.503854,417.7758056,1073.584312,400.2791473,1073.584312,384.7188527,1210.503854,362.2221944 +-55,5,-5,1202.91045,536.2866412,1058.913199,507.7071628,1088.209854,491.8131779,1198.139742,480.3271997 +-55,10,-5,1215.359752,655.2054023,1044.196301,615.4704308,1102.790037,598.575365,1185.817898,598.0284559 +-55,15,-5,1227.851979,774.5341989,1029.433405,723.5705229,1117.325071,705.0069567,1173.538105,715.3280297 +-50,-15,-5,1262.697899,65.18462232,1207.994633,79.56356655,1120.581598,59.11170891,1357.788825,8.25692558 +-50,-10,-5,1275.106589,181.9422688,1193.539231,186.4452834,1135.264585,167.6761575,1345.166476,127.0249606 +-50,-5,-5,1287.557574,299.0978886,1179.038736,293.6604021,1149.901627,275.9008955,1332.587299,245.3867854 +-50,0,-5,1300.051072,416.6535198,1164.492939,401.2104851,1164.492939,383.7875149,1320.051072,363.3444802 +-50,5,-5,1312.587299,534.6112146,1149.901627,509.0971045,1179.038736,491.3375979,1307.557574,480.9001114 +-50,10,-5,1325.166476,652.9730394,1135.264585,617.3218425,1193.539231,598.5527166,1295.106589,598.0557312 +-50,15,-5,1337.788825,771.7410744,1120.581598,725.8862911,1207.994633,705.4344334,1282.697899,714.8133777 +-45,-15,-5,1370.853775,65.69454117,1299.434319,79.13245896,1212.511303,56.77608519,1466.704244,11.02409903 +-45,-10,-5,1383.388474,181.9152448,1285.06043,186.4681244,1227.112466,165.8088966,1453.954723,129.236618 +-45,-5,-5,1395.965701,298.5302637,1270.641513,294.1400342,1241.667745,274.4990738,1441.248606,247.0466985 +-45,0,-5,1408.585672,415.541608,1256.177355,402.1497706,1256.177355,382.8482294,1428.585672,364.456392 +-45,5,-5,1421.248606,532.9513015,1241.667745,510.4989262,1270.641513,490.8579658,1415.965701,481.4677363 +-45,10,-5,1433.954723,650.761382,1227.112466,619.1891034,1285.06043,598.5298756,1403.388474,598.0827552 +-45,15,-5,1446.704244,768.973901,1212.511303,728.2219148,1299.434319,705.865541,1390.853775,714.3034588 +-40,-15,-5,1478.019519,66.19979188,1391.653983,78.69767403,1305.232613,54.42034944,1574.612406,13.76568147 +-40,-10,-5,1490.677402,181.8884686,1377.363529,186.4911606,1319.749997,163.9255821,1561.738596,131.4278565 +-40,-5,-5,1503.378033,297.9678528,1363.028116,294.6237704,1334.221565,273.0852191,1548.908412,248.6913126 +-40,0,-5,1516.12163,414.4399271,1348.647533,403.0971061,1348.647533,381.9008939,1536.12163,365.5580729 +-40,5,-5,1528.908412,531.3066874,1334.221565,511.9127809,1363.028116,490.3742296,1523.378033,482.0301472 +-40,10,-5,1541.738596,648.5701435,1319.749997,621.0724179,1377.363529,598.5068394,1510.677402,598.1095314 +-40,15,-5,1554.612406,766.2323185,1305.232613,730.5776506,1391.653983,706.300326,1498.019519,713.7982081 +-35,-15,-5,1584.208664,66.70043825,1484.663646,78.25916451,1398.755797,52.04424077,1681.52722,16.48202628 +-35,-10,-5,1596.986968,181.8619368,1470.45859,186.5143944,1413.187407,162.0260061,1668.531942,133.5989577 +-35,-5,-5,1609.80823,297.4105844,1456.208651,295.1116636,1427.573275,271.6591758,1655.580498,250.3208383 +-35,0,-5,1622.672667,413.3483364,1441.913617,404.0525955,1441.913617,380.9454045,1642.672667,366.6496636 +-35,5,-5,1635.580498,529.6771617,1427.573275,513.3388242,1456.208651,489.8863364,1629.80823,482.5874156 +-35,10,-5,1648.531942,646.3990423,1413.187407,622.9719939,1470.45859,598.4836056,1616.986968,598.1360632 +-35,15,-5,1661.52722,763.5159737,1398.755797,732.9537592,1484.663646,706.7388355,1604.208664,713.2975618 +-30,-15,-5,1689.434499,67.19654294,1578.473506,77.81688234,1493.091303,49.64749374,1787.462339,19.17348034 +-30,-10,-5,1702.330521,181.8356461,1564.355849,186.5378285,1507.4351,160.1099572,1774.34835,135.7501978 +-30,-5,-5,1715.2697,296.8583882,1550.193395,295.6037676,1521.733235,270.2207857,1761.278395,251.9354823 +-30,0,-5,1728.252251,412.2666981,1535.985928,405.0163445,1535.985928,379.9816555,1748.252251,367.7313019 +-30,5,-5,1741.278395,528.0625177,1521.733235,514.7772143,1550.193395,489.3942324,1735.2697,483.1396118 +-30,10,-5,1754.34835,644.2478022,1507.4351,624.8880428,1564.355849,598.4601715,1722.330521,598.1623539 +-30,15,-5,1767.462339,760.8245197,1493.091303,735.3505063,1578.473506,707.1811177,1709.434499,712.8014571 +-25,-15,-5,1793.710075,67.68816747,1673.093931,77.37077862,1588.249761,47.22983829,1892.431167,21.84038422 +-25,-10,-5,1806.72117,181.8095933,1659.065721,186.5614653,1602.503661,158.1772201,1879.201167,137.8818481 +-25,-5,-5,1819.775608,296.3111953,1644.992804,296.1001373,1616.71199,268.7698876,1866.015388,253.5354474 +-25,0,-5,1832.873609,411.1948766,1630.874965,405.9884607,1630.874965,379.0095393,1852.873609,368.8031234 +-25,5,-5,1846.015388,526.4625526,1616.71199,516.2281124,1644.992804,488.8978627,1839.775608,483.6868047 +-25,10,-5,1859.201167,642.1161519,1602.503661,626.8207799,1659.065721,598.4365347,1826.72117,598.1884067 +-25,15,-5,1872.431167,758.1576158,1588.249761,737.7681617,1673.093931,707.6272214,1813.710075,712.3098325 +-20,-15,-5,1897.048205,68.17537226,1768.535474,76.9208036,1684.241986,44.79099963,1996.446866,24.4830723 +-20,-10,-5,1910.171784,181.783775,1754.5988,186.5853076,1698.403863,156.2275758,1983.103495,139.9941751 +-20,-5,-5,1923.338885,295.7689381,1740.617517,296.6008282,1712.520265,267.3063178,1969.804522,255.1209331 +-20,0,-5,1936.549725,410.1327388,1726.591411,406.9690534,1726.591411,378.0289466,1956.549725,369.8652612 +-20,5,-5,1949.804522,524.8770669,1712.520265,517.6916822,1740.617517,488.3971718,1943.338885,484.2290619 +-20,10,-5,1963.103495,640.0038249,1698.403863,628.7704242,1754.5988,598.4126924,1930.171784,598.214225 +-20,15,-5,1976.446866,755.5149277,1684.241986,740.2070004,1768.535474,708.0771964,1917.048205,711.8226277 +-15,-15,-5,1999.461474,68.65821664,1864.80887,76.46690668,1781.078984,42.33069812,2099.522359,27.1018729 +-15,-10,-5,2012.695005,181.7581882,1850.965866,186.609358,1795.146665,154.2608014,2086.068199,142.0874403 +-15,-5,-5,2025.972226,295.23155,1837.078359,297.1058971,1809.168975,265.8299095,2072.658604,256.6921348 +-15,0,-5,2039.293353,409.0801542,1823.146134,407.9582342,1823.146134,377.0397658,2059.293353,370.9178458 +-15,5,-5,2052.658604,523.3058652,1809.168975,519.1680905,1837.078359,487.8921029,2045.972226,484.76645 +-15,10,-5,2066.068199,637.9105597,1795.146665,630.7371986,1850.965866,598.388642,2032.695005,598.2398118 +-15,15,-5,2079.522359,752.8961271,1781.078984,742.6673019,1864.80887,708.5310933,2019.461474,711.3397834 +-10,-15,-5,2100.962243,69.13675888,1961.925041,76.00903635,1878.771957,39.84864919,2201.670336,29.69710846 +-10,-10,-5,2114.303248,181.7328297,1948.177887,186.6336193,1892.743222,152.2766703,2188.107914,144.1619005 +-10,-5,-5,2127.6881,294.6989657,1934.386343,297.6154016,1906.669229,264.3404929,2174.590215,258.2492449 +-10,0,-5,2141.117017,408.0369944,1920.550196,408.9561162,1920.550196,376.0418838,2161.117017,371.9610056 +-10,5,-5,2154.590215,521.7487551,1906.669229,520.6575071,1934.386343,487.3825984,2147.6881,485.2990343 +-10,10,-5,2168.107914,635.8360995,1892.743222,632.7213297,1948.177887,598.3643807,2134.303248,598.2651703 +-10,15,-5,2181.670336,750.3008915,1878.771957,745.1493508,1961.925041,708.9889637,2120.962243,710.8612411 +-5,-15,-5,2201.562653,69.61105623,2059.895103,75.54714021,1977.332304,37.3445632,2302.90326,32.26909562 +-5,-10,-5,2215.008707,181.7076966,2046.246025,186.6580943,1991.204885,150.2749517,2289.235049,146.217808 +-5,-5,-5,2228.498756,294.1711212,2032.55268,298.1294005,2005.032328,262.8378955,2275.611709,259.792452 +-5,0,-5,2242.033017,407.0031334,2018.814854,409.9628148,2018.814854,375.0351852,2262.033017,372.9948666 +-5,5,-5,2255.611709,520.205548,2005.032328,522.1601045,2032.55268,486.8685995,2248.498756,485.8268788 +-5,10,-5,2269.235049,633.780192,1991.204885,634.7230483,2046.246025,598.3399057,2235.008707,598.2903034 +-5,15,-5,2282.90326,747.7289044,1977.332304,747.6534368,2059.895103,709.4508598,2221.562653,710.3869438 +0,-15,-5,2301.274632,70.08116493,2158.730368,75.08116493,2076.771628,34.81814537,2403.233372,34.81814537 +0,-10,-5,2314.82336,181.6827857,2145.18164,186.6827857,2090.543207,148.2554106,2389.461793,148.2554106 +0,-5,-5,2328.416223,293.6479534,2131.588777,298.6479534,2104.269779,261.3219415,2375.735221,261.3219415 +0,0,-5,2342.053437,405.9784474,2117.951563,410.9784474,2117.951563,374.0195526,2362.053437,374.0195526 +0,5,-5,2355.735221,518.6760585,2104.269779,523.6760585,2131.588777,486.3500466,2348.416223,486.3500466 +0,10,-5,2369.461793,631.7425894,2090.543207,636.7425894,2145.18164,598.3152143,2334.82336,598.3152143 +0,15,-5,2383.233372,745.1798546,2076.771628,750.1798546,2158.730368,709.9168351,2321.274632,709.9168351 +5,-15,-5,2400.109897,70.54714021,2258.442347,74.61105623,2177.10174,32.26909562,2502.672696,37.3445632 +5,-10,-5,2413.758975,181.6580943,2244.996293,186.7076966,2190.769951,146.217808,2488.800115,150.2749517 +5,-5,-5,2427.45232,293.1294005,2231.506244,299.1711212,2204.393291,259.792452,2474.972672,262.8378955 +5,0,-5,2441.190146,404.9628148,2217.971983,412.0031334,2217.971983,372.9948666,2461.190146,375.0351852 +5,5,-5,2454.972672,517.1601045,2204.393291,525.205548,2231.506244,485.8268788,2447.45232,486.8685995 +5,10,-5,2468.800115,629.7230483,2190.769951,638.780192,2244.996293,598.2903034,2433.758975,598.3399057 +5,15,-5,2482.672696,742.6534368,2177.10174,752.7289044,2258.442347,710.3869438,2420.109897,709.4508598 +10,-15,-5,2498.079959,71.00903635,2359.042757,74.13675888,2278.334664,29.69710846,2601.233043,39.84864919 +10,-10,-5,2511.827113,181.6336193,2345.701752,186.7328297,2291.897086,144.1619005,2587.261778,152.2766703 +10,-5,-5,2525.618657,292.6154016,2332.3169,299.6989657,2305.414785,258.2492449,2573.335771,264.3404929 +10,0,-5,2539.454804,403.9561162,2318.887983,413.0369944,2318.887983,371.9610056,2559.454804,376.0418838 +10,5,-5,2553.335771,515.6575071,2305.414785,526.7487551,2332.3169,485.2990343,2545.618657,487.3825984 +10,10,-5,2567.261778,627.7213297,2291.897086,640.8360995,2345.701752,598.2651703,2531.827113,598.3643807 +10,15,-5,2581.233043,740.1493508,2278.334664,755.3008915,2359.042757,710.8612411,2518.079959,708.9889637 +15,-15,-5,2595.19613,71.46690668,2460.543526,73.65821664,2380.482641,27.1018729,2698.926016,42.33069812 +15,-10,-5,2609.039134,181.609358,2447.309995,186.7581882,2393.936801,142.0874403,2684.858335,154.2608014 +15,-5,-5,2622.926641,292.1058971,2434.032774,300.23155,2407.346396,256.6921348,2670.836025,265.8299095 +15,0,-5,2636.858866,402.9582342,2420.711647,414.0801542,2420.711647,370.9178458,2656.858866,377.0397658 +15,5,-5,2650.836025,514.1680905,2407.346396,528.3058652,2434.032774,484.76645,2642.926641,487.8921029 +15,10,-5,2664.858335,625.7371986,2393.936801,642.9105597,2447.309995,598.2398118,2629.039134,598.388642 +15,15,-5,2678.926016,737.6673019,2380.482641,757.8961271,2460.543526,711.3397834,2615.19613,708.5310933 +20,-15,-5,2691.469526,71.9208036,2562.956795,73.17537226,2483.558134,24.4830723,2795.763014,44.79099963 +20,-10,-5,2705.4062,181.5853076,2549.833216,186.783775,2496.901505,139.9941751,2781.601137,156.2275758 +20,-5,-5,2719.387483,291.6008282,2536.666115,300.7689381,2510.200478,255.1209331,2767.484735,267.3063178 +20,0,-5,2733.413589,401.9690534,2523.455275,415.1327388,2523.455275,369.8652612,2753.413589,378.0289466 +20,5,-5,2747.484735,512.6916822,2510.200478,529.8770669,2536.666115,484.2290619,2739.387483,488.3971718 +20,10,-5,2761.601137,623.7704242,2496.901505,645.0038249,2549.833216,598.214225,2725.4062,598.4126924 +20,15,-5,2775.763014,735.2070004,2483.558134,760.5149277,2562.956795,711.8226277,2711.469526,708.0771964 +25,-15,-5,2786.911069,72.37077862,2666.294925,72.68816747,2587.573833,21.84038422,2891.755239,47.22983829 +25,-10,-5,2800.939279,181.5614653,2653.28383,186.8095933,2600.803833,137.8818481,2877.501339,158.1772201 +25,-5,-5,2815.012196,291.1001373,2640.229392,301.3111953,2613.989612,253.5354474,2863.29301,268.7698876 +25,0,-5,2829.130035,400.9884607,2627.131391,416.1948766,2627.131391,368.8031234,2849.130035,379.0095393 +25,5,-5,2843.29301,511.2281124,2613.989612,531.4625526,2640.229392,483.6868047,2835.012196,488.8978627 +25,10,-5,2857.501339,621.8207799,2600.803833,647.1161519,2653.28383,598.1884067,2820.939279,598.4365347 +25,15,-5,2871.755239,732.7681617,2587.573833,763.1576158,2666.294925,712.3098325,2806.911069,707.6272214 +30,-15,-5,2881.531494,72.81688234,2770.570501,72.19654294,2692.542661,19.17348034,2986.913697,49.64749374 +30,-10,-5,2895.649151,181.5378285,2757.674479,186.8356461,2705.65665,135.7501978,2972.5699,160.1099572 +30,-5,-5,2909.811605,290.6037676,2744.7353,301.8583882,2718.726605,251.9354823,2958.271765,270.2207857 +30,0,-5,2924.019072,400.0163445,2731.752749,417.2666981,2731.752749,367.7313019,2944.019072,379.9816555 +30,5,-5,2938.271765,509.7772143,2718.726605,533.0625177,2744.7353,483.1396118,2929.811605,489.3942324 +30,10,-5,2952.5699,619.8880428,2705.65665,649.2478022,2757.674479,598.1623539,2915.649151,598.4601715 +30,15,-5,2966.913697,730.3505063,2692.542661,765.8245197,2770.570501,712.8014571,2901.531494,707.1811177 +35,-15,-5,2975.341354,73.25916451,2875.796336,71.70043825,2798.47778,16.48202628,3081.249203,52.04424077 +35,-10,-5,2989.54641,181.5143944,2863.018032,186.8619368,2811.473058,133.5989577,3066.817593,162.0260061 +35,-5,-5,3003.796349,290.1116636,2850.19677,302.4105844,2824.424502,250.3208383,3052.431725,271.6591758 +35,0,-5,3018.091383,399.0525955,2837.332333,418.3483364,2837.332333,366.6496636,3038.091383,380.9454045 +35,5,-5,3032.431725,508.3388242,2824.424502,534.6771617,2850.19677,482.5874156,3023.796349,489.8863364 +35,10,-5,3046.817593,617.9719939,2811.473058,651.3990423,2863.018032,598.1360632,3009.54641,598.4836056 +35,15,-5,3061.249203,727.9537592,2798.47778,768.5159737,2875.796336,713.2975618,2995.341354,706.7388355 +40,-15,-5,3068.351017,73.69767403,2981.985481,71.19979188,2905.392594,13.76568147,3174.772387,54.42034944 +40,-10,-5,3082.641471,181.4911606,2969.327598,186.8884686,2918.266404,131.4278565,3160.255003,163.9255821 +40,-5,-5,3096.976884,289.6237704,2956.626967,302.9678528,2931.096588,248.6913126,3145.783435,273.0852191 +40,0,-5,3111.357467,398.0971061,2943.88337,419.4399271,2943.88337,365.5580729,3131.357467,381.9008939 +40,5,-5,3125.783435,506.9127809,2931.096588,536.3066874,2956.626967,482.0301472,3116.976884,490.3742296 +40,10,-5,3140.255003,616.0724179,2918.266404,653.5701435,2969.327598,598.1095314,3102.641471,598.5068394 +40,15,-5,3154.772387,725.5776506,2905.392594,771.2323185,2981.985481,713.7982081,3088.351017,706.300326 +45,-15,-5,3160.570681,74.13245896,3089.151225,70.69454117,3013.300756,11.02409903,3267.493697,56.77608519 +45,-10,-5,3174.94457,181.4681244,3076.616526,186.9152448,3026.050277,129.236618,3252.892534,165.8088966 +45,-5,-5,3189.363487,289.1400342,3064.039299,303.5302637,3038.756394,247.0466985,3238.337255,274.4990738 +45,0,-5,3203.827645,397.1497706,3051.419328,420.541608,3051.419328,364.456392,3223.827645,382.8482294 +45,5,-5,3218.337255,505.4989262,3038.756394,537.9513015,3064.039299,481.4677363,3209.363487,490.8579658 +45,10,-5,3232.892534,614.1891034,3026.050277,655.761382,3076.616526,598.0827552,3194.94457,598.5298756 +45,15,-5,3247.493697,723.2219148,3013.300756,773.973901,3089.151225,714.3034588,3180.570681,705.865541 +50,-15,-5,3252.010367,74.56356655,3197.307101,70.18462232,3122.216175,8.25692558,3359.423402,59.11170891 +50,-10,-5,3266.465769,181.4452834,3184.898411,186.9422688,3134.838524,127.0249606,3344.740415,167.6761575 +50,-5,-5,3280.966264,288.6604021,3172.447426,304.0978886,3147.417701,245.3867854,3330.103373,275.9008955 +50,0,-5,3295.512061,396.2104851,3159.953928,421.6535198,3159.953928,363.3444802,3315.512061,383.7875149 +50,5,-5,3310.103373,504.0971045,3147.417701,539.6112146,3172.447426,480.9001114,3300.966264,491.3375979 +50,10,-5,3324.740415,612.3218425,3134.838524,657.9730394,3184.898411,598.0557312,3286.465769,598.5527166 +50,15,-5,3339.423402,720.8862911,3122.216175,776.7410744,3197.307101,714.8133777,3272.010367,705.4344334 +55,-15,-5,3342.679929,74.99104327,3306.466895,69.66997033,3232.153021,5.463801135,3450.571595,61.42747708 +55,-10,-5,3357.214963,181.422635,3294.187102,186.9695441,3244.645248,124.7925977,3435.808699,169.5275692 +55,-5,-5,3371.795146,288.1848221,3281.865258,304.6708003,3257.09455,243.7113588,3421.091801,277.2908372 +55,0,-5,3386.420688,395.2791473,3269.501146,422.7758056,3269.501146,362.2221944,3406.420688,384.7188527 +55,5,-5,3401.091801,502.7071628,3257.09455,541.2866412,3281.865258,480.3271997,3391.795146,491.8131779 +55,10,-5,3415.808699,610.4704308,3244.645248,660.2054023,3294.187102,598.0284559,3377.214963,598.575365 +55,15,-5,3430.571595,718.5705229,3232.153021,779.5341989,3306.466895,715.3280297,3362.679929,705.0069567 +60,-15,-5,3432.589055,75.41493479,3416.644651,69.15051898,3343.125729,2.644358914,3540.948201,63.7236418 +60,-10,-5,3447.201878,181.4001769,3404.496707,186.9970742,3355.48482,122.539237,3526.107269,171.3633328 +60,-5,-5,3461.8599,287.7132431,3392.306969,305.249073,3367.801243,242.0202003,3511.312384,278.6690493 +60,0,-5,3476.563332,394.3556568,3380.07522,423.9086113,3380.07522,361.0893887,3496.563332,385.6423432 +60,5,-5,3491.312384,501.3289507,3367.801243,542.9777997,3392.306969,479.748927,3481.8599,492.2847569 +60,10,-5,3506.107269,608.6346672,3355.48482,662.458763,3404.496707,598.0009258,3467.201878,598.5978231 +60,15,-5,3520.948201,716.2743582,3343.125729,782.3536411,3416.644651,715.847481,3452.589055,704.5830652 +65,-15,-5,3521.747272,75.83528602,3527.854675,68.62620084,3455.14901,-0.201774808,3630.562975,66.00045096 +65,-10,-5,3536.436079,181.3779067,3515.841598,187.0248626,3467.371881,120.264581,3615.645843,173.1836457 +65,-5,-5,3551.170129,287.2456148,3503.786997,305.8327823,3479.552356,240.3130873,3600.7748,280.0356796 +65,0,-5,3565.949633,393.4399149,3491.690655,425.0520856,3491.690655,359.9459144,3585.949633,386.5580851 +65,5,-5,3580.7748,499.9623204,3479.552356,544.6849127,3503.786997,479.1652177,3571.170129,492.7523852 +65,10,-5,3595.645843,606.8143543,3467.371881,664.733419,3515.841598,597.9731374,3556.436079,598.6200933 +65,15,-5,3610.562975,713.997549,3455.14901,785.1997748,3527.854675,716.3717992,3541.747272,704.162714 +70,-15,-5,3610.163948,76.25214114,3640.111541,68.09694719,3568.237853,-3.074980863,3719.425511,68.25814826 +70,-10,-5,3624.92697,181.3558219,3628.236418,187.0529131,3580.321351,117.9683262,3704.433976,174.9887022 +70,-5,-5,3639.735274,286.7818878,3616.320054,306.4220053,3592.362737,238.5897928,3689.488567,281.3908735 +70,0,-5,3654.58907,392.5318244,3604.362232,426.2063797,3604.362232,358.7916203,3674.58907,387.4661756 +70,5,-5,3669.488567,498.6071265,3592.362737,546.4082072,3616.320054,478.5759947,3659.735274,493.2161122 +70,10,-5,3684.433976,605.0092978,3580.321351,667.0296738,3628.236418,597.9450869,3644.92697,598.6421781 +70,15,-5,3699.425511,711.7398517,3568.237853,788.0729809,3640.111541,716.9010528,3630.163948,703.7458589 +75,-15,-5,3697.848296,76.66554357,3753.430102,67.562688,3682.407533,-5.975647363,3807.545239,70.49697334 +75,-10,-5,3712.6838,181.3339203,3741.696089,187.0812294,3694.348433,115.6501637,3792.481063,176.7786932 +75,-5,-5,3727.564621,286.3220134,3729.921131,307.0168205,3706.247519,236.8500858,3777.463042,282.734774 +75,0,-5,3742.490965,391.6312899,3718.105013,427.3716481,3718.105013,357.6263519,3762.490965,388.3667101 +75,5,-5,3757.463042,497.263226,3706.247519,548.1479142,3729.921131,477.9811795,3747.564621,493.6759866 +75,10,-5,3772.481063,603.2193068,3694.348433,669.3478363,3741.696089,597.9167706,3732.6838,598.6640797 +75,15,-5,3787.545239,709.5010267,3682.407533,790.9736474,3753.430102,717.435312,3717.848296,703.3324564 +80,-15,-5,3784.809378,77.07553604,3867.825491,67.02335193,3797.673621,-8.904169876,3894.931436,72.71716185 +80,-10,-5,3799.715667,181.3121997,3856.235815,187.1098152,3809.468623,113.3097784,3879.796342,178.5538067 +80,-5,-5,3814.6673,285.8659438,3844.605505,307.6173078,3821.222125,235.0937306,3864.707429,284.0675216 +80,0,-5,3829.664485,390.7382174,3832.934347,428.548048,3832.934347,356.449952,3849.664485,389.2597826 +80,5,-5,3844.707429,495.9304784,3821.222125,549.9042694,3844.605505,477.3806922,3834.6673,494.1320562 +80,10,-5,3859.796342,601.4441933,3809.468623,671.6882216,3856.235815,597.8881848,3819.715667,598.6858003 +80,15,-5,3874.931436,707.2808381,3797.673621,793.9021699,3867.825491,717.9746481,3804.809378,702.922464 +85,-15,-5,3871.056104,77.48216056,3983.313129,66.47886627,3914.051986,-11.8609516,3981.593221,74.91894552 +85,-10,-5,3886.031516,181.2906578,3971.871092,187.1386744,3925.697715,110.9468492,3966.3889,180.3142272 +85,-5,-5,3901.052294,285.4136321,3960.388746,308.2235488,3937.302273,233.3204871,3951.230778,285.3892547 +85,0,-5,3916.118646,389.8525146,3948.865878,429.7357396,3948.865878,355.2622604,3936.118646,390.1454854 +85,5,-5,3931.230778,494.6087453,3937.302273,551.6775129,3960.388746,476.7744512,3921.052294,494.5843679 +85,10,-5,3946.3889,599.6837728,3925.697715,674.0511508,3971.871092,597.8593256,3906.031516,598.7073422 +85,15,-5,3961.593221,705.0790545,3914.051986,796.8589516,3983.313129,718.5191337,3891.056104,702.5158394 +90,-15,-5,3956.597241,77.88545847,4099.908736,65.9291569,4031.558806,-14.84640356,4067.539566,77.10255227 +90,-10,-5,3971.640147,181.2692924,4088.617713,187.1678109,4043.05181,108.5610487,4052.267672,182.0601366 +90,-5,-5,3986.728436,284.9650319,4077.286723,308.8356265,4054.503986,231.5301105,4037.041991,286.7001091 +90,0,-5,4001.862315,388.9740906,4065.915552,430.9348864,4065.915552,354.0631136,4021.862315,391.0239094 +90,5,-5,4017.041991,493.2978909,4054.503986,553.4678895,4077.286723,476.1623735,4006.728436,495.0329681 +90,10,-5,4032.267672,597.9378634,4043.05181,676.4369513,4088.617713,597.8301891,3991.640147,598.7287076 +90,15,-5,4047.539566,702.8954477,4031.558806,799.8444036,4099.908736,719.0688431,3976.597241,702.1125415 +95,-15,-5,4041.441414,78.28547042,4217.628332,65.3741483,4150.210574,-17.86094479,4152.779295,79.26820626 +95,-10,-5,4056.550216,181.2481013,4206.491778,187.1972289,4161.54732,106.1520434,4137.441448,183.7917134 +95,-5,-5,4071.704416,284.5200977,4195.315613,309.4536257,4172.843597,229.7223514,4122.149825,288.0002187 +95,0,-5,4086.904216,388.1028561,4184.099623,432.1456547,4184.099623,352.8523453,4106.904216,391.8951439 +95,5,-5,4102.149825,491.9977813,4172.843597,555.2756486,4195.315613,475.5443743,4091.704416,495.4779023 +95,10,-5,4117.441448,596.2062866,4161.54732,678.8459566,4206.491778,597.8007711,4076.550216,598.7498987 +95,15,-5,4132.779295,700.7297937,4150.210574,802.8589448,4217.628332,719.6238517,4061.441414,701.7125296 +100,-15,-5,4125.597104,78.68223641,4336.488249,64.81376347,4270.024106,-20.90500251,4237.321084,81.41612799 +100,-10,-5,4140.77024,181.2270824,4325.509698,187.2269323,4281.200979,103.7194929,4221.918874,185.5091335 +100,-5,-5,4155.98878,284.0787848,4314.491906,310.0776327,4292.33776,227.8969553,4206.562891,289.289715 +100,0,-5,4171.252929,387.2387231,4303.434664,433.3682145,4303.434664,351.6297855,4191.252929,392.7592769 +100,5,-5,4186.562891,490.708285,4292.33776,557.1010447,4314.491906,474.9203673,4175.98878,495.9192152 +100,10,-5,4201.918874,594.4888665,4281.200979,681.2785071,4325.509698,597.7710677,4160.77024,598.7709176 +100,15,-5,4217.321084,698.581872,4270.024106,805.9030025,4336.488249,720.1842365,4145.597104,701.3157636 +105,-15,-5,4209.072659,79.07579579,4456.505137,64.2479239,4391.016548,-23.97901238,4321.173473,83.54653435 +105,-10,-5,4224.308595,181.2062336,4445.688203,187.2569254,4402.029849,101.2630505,4305.708454,187.2125698 +105,-5,-5,4239.589939,283.6410492,4434.832417,310.7077355,4413.003452,226.0536629,4290.289663,290.5687275 +105,0,-5,4254.916894,386.3816052,4423.93757,434.6027388,4423.93757,350.3952612,4274.916894,393.6163948 +105,5,-5,4270.289663,489.4292725,4413.003452,558.9443371,4434.832417,474.2902645,4259.589939,496.3569508 +105,10,-5,4285.708454,592.7854302,4402.029849,683.7349495,4445.688203,597.7410746,4244.308595,598.7917664 +105,15,-5,4301.173473,696.4514657,4391.016548,808.9770124,4456.505137,720.7500761,4229.072659,700.9222042 +-105,-15,0,51.95690139,28.4915937,188.1576111,58.67392749,94.99001382,105.8325282,138.4035336,2.23821601 +-105,-10,0,62.93143489,152.2430704,172.7458215,161.3156762,110.6347892,210.0259506,127.2284634,128.2509921 +-105,-5,0,73.94564155,276.4419112,157.287853,264.2649742,126.2325466,313.9062361,116.093938,253.8065769 +-105,0,0,84.99973688,401.0905461,141.7834978,367.5232059,141.7834978,417.4747941,104.9997369,378.9074539 +-105,5,0,96.09393796,526.1914231,126.2325466,471.0917639,157.287853,520.7330258,93.94564155,503.5560888 +-105,10,0,107.2284634,651.7470079,110.6347892,574.9720494,172.7458215,623.6823238,82.93143489,627.7549296 +-105,15,0,118.4035336,777.759784,94.99001382,679.1654718,188.1576111,726.3240725,71.95690139,751.5064063 +-100,-15,0,172.5109483,29.21100389,271.8272274,58.17462633,179.035885,103.7822643,259.9406075,5.203062151 +-100,-10,0,183.6483367,152.3594517,256.4781505,161.2349512,194.6179576,208.4070231,248.600713,130.5906619 +-100,-5,0,194.82579,275.9509055,241.0828944,264.6053431,210.1530086,312.7160582,237.3017593,255.525572 +-100,0,0,206.0435248,399.9877603,225.6412504,368.2872033,225.6412504,416.7107967,226.0435248,380.0102397 +-100,5,0,217.3017593,524.472428,210.1530086,472.2819418,241.0828944,520.3926569,214.82579,504.0470945 +-100,10,0,228.600713,649.4073381,194.6179576,576.5909769,256.4781505,623.7630488,203.6483367,627.6385483 +-100,15,0,239.9406075,774.7949378,179.035885,681.2157357,271.8272274,726.8233737,192.5109483,750.7869961 +-95,-15,0,291.8953938,29.92343446,356.1822515,57.67123498,263.7765238,101.7150518,380.285788,8.138832551 +-95,-10,0,303.1919645,152.4747017,340.8974355,161.153564,279.2943219,206.7747329,368.7848276,132.9074282 +-95,-5,0,314.5289749,275.4646806,325.5664447,264.9485086,294.7650994,311.5160715,357.3251866,257.2277698 +-95,0,0,325.9066426,398.8957311,310.1890696,369.0574877,310.1890696,415.9405123,345.9066426,381.1022689 +-95,5,0,337.3251866,522.7702302,294.7650994,473.4819285,325.5664447,520.0494914,334.5289749,504.5333194 +-95,10,0,348.7848276,647.0905718,279.2943219,578.2232671,340.8974355,623.844436,323.1919645,627.5232983 +-95,15,0,360.285788,771.8591674,263.7765238,683.2829482,356.1822515,727.326765,311.8953938,750.0745655 +-90,-15,0,410.1271765,30.62898647,441.2311402,57.16370297,349.2205809,99.63067976,499.4565226,11.04595284 +-90,-10,0,421.5793404,152.5888371,426.0121652,161.0715062,364.6725001,205.128914,487.7981686,135.2016257 +-90,-5,0,433.0723024,274.9831669,410.7470248,265.2945054,380.0774045,310.3061542,476.1814957,258.9134153 +-90,0,0,444.6062808,397.814302,395.4355085,369.8341369,395.4355085,415.1638631,464.6062808,382.183698 +-90,5,0,456.1814957,521.0845847,380.0774045,474.6918458,410.7470248,519.7034946,453.0723024,505.0148331 +-90,10,0,467.7981686,644.7963743,364.6725001,579.869086,426.0121652,623.9264938,441.5793404,627.4091629 +-90,15,0,479.4565226,768.9520472,349.2205809,685.3673202,441.2311402,727.834297,430.1271765,749.3690135 +-85,-15,0,527.2229099,31.32775907,526.9824898,56.651979,435.3768514,97.52893354,617.46992,13.92484036 +-85,-10,0,538.827159,152.7018738,511.8309689,160.9887697,450.7612537,203.4693975,605.6577607,137.4735826 +-85,-5,0,550.4725487,274.5062964,496.6332966,265.6433687,466.0986521,309.0861826,593.887628,260.5827488 +-85,0,0,562.1592981,396.7433193,481.3892619,370.6172302,481.3892619,414.3807698,582.1592981,383.2546807 +-85,5,0,573.887628,519.4152512,466.0986521,475.9118174,496.6332966,519.3546313,570.4725487,505.4917036 +-85,10,0,585.6577607,642.5244174,450.7612537,581.5286025,511.8309689,624.0092303,558.827159,627.2961262 +-85,15,0,597.46992,766.0731596,435.3768514,687.4690665,526.9824898,728.346021,547.2229099,748.6702409 +-80,-15,0,643.1988896,32.0198495,613.4450396,56.13601092,522.2542774,95.40959503,734.342758,16.77590442 +-80,-10,0,654.9517951,152.8138277,598.3626189,160.905346,537.5694905,201.7960117,722.3803,139.7236207 +-80,-5,0,666.7461682,274.0340021,583.2340662,265.9951342,552.837716,307.8560307,710.4601985,262.2360058 +-80,0,0,678.5822289,395.6826324,568.0591697,371.4068481,568.0591697,413.5911519,698.5822289,384.3153676 +-80,5,0,690.4601985,517.7619942,552.837716,477.1419693,583.2340662,519.0028658,686.7461682,505.9639979 +-80,10,0,702.3803,640.2743793,537.5694905,583.2019883,598.3626189,624.092654,674.9517951,627.1841723 +-80,15,0,714.342758,763.2220956,522.2542774,689.588405,613.4450396,728.8619891,663.1988896,747.9781505 +-75,-15,0,758.0711009,32.70535316,700.627674,55.61574572,609.8619513,93.27244242,850.0914918,19.59954642 +-75,-10,0,769.969311,152.9247142,685.6160341,160.8212264,625.106268,200.1085819,837.9821623,141.9520558 +-75,-5,0,781.9093009,273.5662185,670.5582869,266.3498383,640.3036186,306.6155706,825.9155038,263.8734175 +-75,0,0,793.8912912,394.6320936,655.4542197,372.2030726,655.4542197,412.7949274,813.8912912,385.3659064 +-75,5,0,805.9155038,516.1245825,640.3036186,478.3824294,670.5582869,518.6481617,801.9093009,506.4317815 +-75,10,0,817.9821623,638.0459442,625.106268,584.8894181,685.6160341,624.1767736,789.969311,627.0732858 +-75,15,0,830.0914918,760.3984536,609.8619513,691.7255576,700.627674,729.3822543,778.0711009,747.2926468 +-70,-15,0,871.8552264,33.38436363,788.5394264,55.09112947,698.2091193,91.11725017,964.7322616,22.3961601 +-70,-10,0,883.8954643,153.0345485,773.6002827,160.7364022,713.3807963,198.4069306,952.4794101,144.1591974 +-70,-5,0,895.9777799,273.1028813,758.615062,266.707518,728.5055343,305.3646723,940.2695295,265.4952106 +-70,0,0,908.1023942,393.5915579,743.5835508,373.0059868,743.5835508,411.9920132,928.1023942,386.4064421 +-70,5,0,920.2695295,514.5027894,728.5055343,479.6333277,758.615062,518.290482,915.9777799,506.8951187 +-70,10,0,932.4794101,635.8388026,713.3807963,586.5910694,773.6002827,624.2615978,903.8954643,626.9634515 +-70,15,0,944.7322616,757.6018399,698.2091193,693.8807498,788.5394264,729.9068705,891.8552264,746.6136364 +-65,-15,0,984.566653,34.05697274,877.1894816,54.56210739,787.3051841,88.94378889,1078.2809,25.1661317 +-65,-10,0,996.7457152,153.1433456,862.3245856,160.6508645,802.4024414,196.6908773,1065.887801,146.3453494 +-65,-5,0,1008.967139,272.6439275,847.4136484,267.0682109,817.4527926,304.1032033,1053.537958,267.1016076 +-65,0,0,1021.231145,392.5608831,832.456456,373.8156754,832.456456,411.1823246,1041.231145,387.4371169 +-65,5,0,1033.537958,512.8963924,817.4527926,480.8947967,847.4136484,517.9297891,1028.967139,507.3540725 +-65,10,0,1045.887801,633.6526506,802.4024414,588.3071227,862.3245856,624.3471355,1016.745715,626.8546544 +-65,15,0,1058.2809,754.8318683,787.3051841,696.0542111,877.1894816,730.4358926,1004.566653,745.9410273 +-60,-15,0,1096.220479,34.72327057,966.5871795,54.02862371,877.1597085,86.75182531,1190.752939,27.9098401 +-60,-10,0,1108.535233,153.2511201,951.7983192,160.5646043,892.1807285,194.9602384,1178.222793,148.5108096 +-60,-5,0,1120.892619,272.189295,936.9634596,267.4319551,907.1548812,302.8310292,1165.736175,268.6928266 +-60,0,0,1133.292858,391.5399296,922.0823857,374.6322246,922.0823857,410.3657754,1153.292858,388.4580704 +-60,5,0,1145.736175,511.3051734,907.1548812,482.1669708,936.9634596,517.5660449,1140.892619,507.808705 +-60,10,0,1158.222793,631.4871904,892.1807285,590.0377616,951.7983192,624.4333957,1128.535233,626.7468799 +-60,15,0,1170.752939,752.0881599,877.1597085,698.2461747,966.5871795,730.9693763,1116.220479,745.2747294 +-55,-15,0,1206.83152,35.38334556,1056.742018,53.49062178,967.7824188,84.54112215,1302.163619,30.62765708 +-55,-10,0,1219.278903,153.3578863,1042.031019,160.4776125,982.7253453,193.2148271,1289.499554,150.6558706 +-55,-5,0,1231.769175,271.7389231,1027.274069,267.7987897,997.6214496,301.5480131,1276.879277,270.2690818 +-55,0,0,1244.302559,390.5285607,1012.470951,375.4557219,1012.470951,409.5422781,1264.302559,389.4694393 +-55,5,0,1256.879277,509.7289182,997.6214496,483.4499869,1027.274069,517.1992103,1251.769175,508.2590769 +-55,10,0,1269.499554,629.3421294,982.7253453,591.7831729,1042.031019,624.5203875,1239.278903,626.6401137 +-55,15,0,1282.163619,749.3703429,967.7824188,700.4568778,1056.742018,731.5073782,1226.83152,744.6146544 +-50,-15,0,1316.414317,36.03728447,1147.663657,52.94804394,1059.183208,82.31143807,1412.527891,33.31994741 +-50,-10,0,1328.991334,153.4636582,1133.032384,160.3898795,1074.046146,191.4544534,1399.732968,152.7808191 +-50,-5,0,1341.611485,271.2927523,1118.355213,268.1687542,1088.862313,300.2540157,1386.982077,271.8305831 +-50,0,0,1354.274991,389.526642,1103.631929,376.2862563,1103.631929,408.7117437,1374.274991,390.471358 +-50,5,0,1366.982077,508.1674169,1088.862313,484.7439843,1118.355213,516.8292458,1361.611485,508.7052477 +-50,10,0,1379.732968,627.2171809,1074.046146,593.5435466,1133.032384,624.6081205,1348.991334,626.5343418 +-50,15,0,1392.527891,746.6780526,1059.183208,702.6865619,1147.663657,732.0499561,1336.414317,743.9607155 +-45,-15,0,1424.983142,36.68517246,1239.361922,52.40083156,1151.372141,80.06252757,1521.86043,35.98706902 +-45,-10,0,1437.686863,153.5684498,1224.812277,160.301396,1166.153154,189.6789241,1508.93764,154.8859368 +-45,-5,0,1450.433951,270.850724,1210.216796,268.5418887,1180.887453,298.9488956,1496.059114,273.3775369 +-45,0,0,1463.224626,388.5340416,1195.575261,377.1239185,1195.575261,407.8740815,1483.224626,391.4639584 +-45,5,0,1476.059114,506.6204631,1180.887453,486.0491044,1210.216796,516.4561113,1470.433951,509.147276 +-45,10,0,1488.93764,625.1120632,1166.153154,595.3190759,1224.812277,624.696604,1457.686863,626.4295502 +-45,15,0,1501.86043,744.010931,1151.372141,704.9354724,1239.361922,732.5971684,1444.983142,743.3128275 +-40,-15,0,1532.552002,37.32709314,1331.846804,51.84892502,1244.359456,77.79414088,1630.175636,38.62937322 +-40,-10,0,1545.379563,153.6722746,1317.380733,160.2121523,1259.056566,187.8880427,1617.127903,156.9714999 +-40,-5,0,1558.250711,270.4127808,1302.868891,268.9182342,1273.707028,297.6325086,1604.124656,274.9101454 +-40,0,0,1571.165667,387.5506301,1288.311063,377.9688006,1288.311063,407.0291994,1591.165667,392.4473699 +-40,5,0,1584.124656,505.0878546,1273.707028,487.3654914,1302.868891,516.0797658,1578.250711,509.5852192 +-40,10,0,1597.127903,623.0265001,1259.056566,597.1099573,1317.380733,624.7858477,1565.379563,626.3257254 +-40,15,0,1610.175636,741.3686268,1244.359456,707.2038591,1331.846804,733.149075,1552.552002,742.6709069 +-35,-15,0,1639.134651,37.96312856,1425.128471,51.29226364,1338.155569,75.50602389,1737.487643,41.24720479 +-35,-10,0,1652.08325,153.7751459,1410.747957,160.1221384,1352.766757,186.0816093,1724.317828,159.0377796 +-35,-5,0,1665.075643,269.9788664,1396.321747,269.2978324,1367.331367,296.3047083,1711.192707,276.4286073 +-35,0,0,1678.112054,386.5762806,1381.849624,378.8209963,1381.849624,406.1770037,1698.112054,393.4217194 +-35,5,0,1691.192707,503.5693927,1367.331367,488.6932917,1396.321747,515.7001676,1685.075643,510.0191336 +-35,10,0,1704.317828,620.9602204,1352.766757,598.9163907,1410.747957,624.8758616,1672.08325,626.2228541 +-35,15,0,1717.487643,738.7507952,1338.155569,709.4919761,1425.128471,733.7057364,1659.134651,742.0348714 +-30,-15,0,1744.744589,38.5933593,1519.217262,50.73078571,1432.771078,73.19791807,1843.810323,43.84090215 +-30,-10,0,1757.811483,153.8770768,1504.924333,160.0313445,1447.29428,184.2594202,1830.521223,161.0850422 +-30,-5,0,1770.922371,269.5489253,1490.58579,269.6807255,1461.770985,294.9653455,1817.277016,277.9331176 +-30,0,0,1784.077473,385.6108683,1476.201414,379.6806011,1476.201414,405.3173989,1804.077473,394.3871317 +-30,5,0,1797.277016,502.0648824,1461.770985,490.0326545,1490.58579,515.3172745,1790.922371,510.4490747 +-30,10,0,1810.521223,618.9129578,1447.29428,600.7385798,1504.924333,624.9666555,1777.811483,626.1209232 +-30,15,0,1823.810323,736.1570979,1432.771078,711.8000819,1519.217262,734.2672143,1764.744589,741.4046407 +-25,-15,0,1849.39507,39.21786446,1614.123699,50.16442846,1528.216768,70.86956034,1949.157295,46.41079751 +-25,-10,0,1862.577579,153.9780801,1599.920427,159.9397603,1542.649877,182.4212685,1935.751647,163.1135491 +-25,-5,0,1875.804268,269.1229033,1585.671628,270.0669567,1557.036575,293.6142686,1922.391077,279.4238676 +-25,0,0,1889.07536,384.6542708,1571.377084,380.5477119,1571.377084,404.4502881,1909.07536,395.3437292 +-25,5,0,1902.391077,500.5741324,1557.036575,491.3837314,1585.671628,514.9310433,1895.804268,510.8750967 +-25,10,0,1915.751647,616.8844509,1542.649877,602.5767315,1599.920427,625.0582397,1882.577579,626.0199199 +-25,15,0,1929.157295,733.5872025,1528.216768,714.1284397,1614.123699,734.8335715,1869.39507,740.7801355 +-20,-15,0,1953.099112,39.8367217,1709.858488,49.593128,1624.503613,68.52068297,2053.541928,48.95721703 +-20,-10,0,1966.394612,154.0781684,1695.746986,159.8473755,1638.844476,180.5669435,2040.022408,165.1235567 +-20,-5,0,1979.734469,268.700747,1681.590054,270.4565698,1653.139021,292.2513233,2026.548142,280.9010453 +-20,0,0,1993.118904,383.706368,1667.387473,381.4224275,1667.387473,403.5755725,2013.118904,396.291632 +-20,5,0,2006.548142,499.0969547,1653.139021,492.7466767,1681.590054,514.5414302,1999.734469,511.297253 +-20,10,0,2020.022408,614.8744433,1638.844476,604.4310565,1695.746986,625.1506245,1986.394612,625.9198316 +-20,15,0,2033.541928,731.040783,1624.503613,716.477317,1709.858488,735.404872,1973.099112,740.1612783 +-15,-15,0,2055.869495,40.4500073,1806.43252,49.01681932,1721.642783,66.15101352,2156.977349,51.48048091 +-15,-10,0,2069.275419,154.1773541,1792.414949,159.7541795,1735.889199,178.6962309,2143.346574,167.1153169 +-15,-5,0,2082.725867,268.282404,1778.352053,270.8496094,1750.0894,290.8763523,2129.76122,282.3648351 +-15,0,0,2096.221059,382.7670419,1764.24361,382.3048485,1764.24361,402.6931515,2116.221059,397.2309581 +-15,5,0,2109.76122,497.6331649,1750.0894,494.1216477,1778.352053,514.1483906,2102.725867,511.715596 +-15,10,0,2123.346574,612.8826831,1735.889199,606.3017691,1792.414949,625.2438205,2089.275419,625.8206459 +-15,15,0,2136.977349,728.5175191,1721.642783,718.8469865,1806.43252,735.9811807,2075.869495,739.5479927 +-10,-15,0,2157.718772,41.05779618,1903.856879,48.43543629,1819.645644,63.76027467,2259.476447,53.98090358 +-10,-10,0,2171.232609,154.2756494,1889.935446,159.6601616,1833.795366,176.8089124,2245.736978,169.0890773 +-10,-5,0,2184.791125,267.867823,1875.9688,271.2461209,1847.898983,289.489196,2232.043086,283.8154183 +-10,0,0,2198.394543,381.8361765,1861.95672,383.195077,1861.95672,401.802923,2218.394543,398.1618235 +-10,5,0,2212.043086,496.1825817,1847.898983,495.508804,1875.9688,513.7518791,2204.791125,512.130177 +-10,10,0,2225.736978,610.9089227,1833.795366,608.1890876,1889.935446,625.3378384,2191.232609,625.7223506 +-10,15,0,2239.476447,726.0170964,1819.645644,721.2377253,1903.856879,736.5625637,2177.718772,738.9402038 +-5,-15,0,2258.65927,41.66016188,2002.142846,47.84891158,1918.523767,61.34818417,2361.051877,56.45879377 +-5,-10,0,2272.278562,154.3730661,1988.319806,159.5653108,1932.574498,174.9047659,2347.206221,171.0450805 +-5,-5,0,2285.942681,267.4569533,1974.451674,271.6461506,1946.579243,288.0896915,2333.406287,285.2529728 +-5,0,0,2299.651848,380.9136581,1960.538227,384.0932172,1960.538227,400.9047828,2319.651848,399.0843419 +-5,5,0,2313.406287,494.7450272,1946.579243,496.9083085,1974.451674,513.3518494,2305.942681,512.5410467 +-5,10,0,2327.206221,608.9529195,1932.574498,610.0932341,1988.319806,625.4326892,2292.278562,625.6249339 +-5,15,0,2341.051877,723.5392062,1918.523767,723.6498158,2002.142846,737.1490884,2278.65927,738.3378381 +0,-15,0,2358.703099,42.25717669,2101.301901,47.25717669,2018.28893,58.9144547,2461.71607,58.9144547 +0,-10,0,2372.425442,154.4696161,2087.579558,159.4696161,2032.238323,172.9835653,2447.766677,172.9835653 +0,-5,0,2386.192749,267.0497454,2073.812251,272.0497454,2046.141857,286.6776734,2433.863143,286.6776734 +0,0,0,2400.005242,379.9993749,2059.999758,384.9993749,2059.999758,399.9986251,2420.005242,399.9986251 +0,5,0,2413.863143,493.3203266,2046.141857,498.3203266,2073.812251,512.9482546,2406.192749,512.9482546 +0,10,0,2427.766677,607.0144347,2032.238323,612.0144347,2087.579558,625.5283839,2392.425442,625.5283839 +0,15,0,2441.71607,721.0835453,2018.28893,726.0835453,2101.301901,737.7408233,2378.703099,737.7408233 +5,-15,0,2457.862154,42.84891158,2201.34573,46.66016188,2118.953123,56.45879377,2561.481233,61.34818417 +5,-10,0,2471.685194,154.5653108,2187.726438,159.3730661,2132.798779,171.0450805,2547.430502,174.9047659 +5,-5,0,2485.553326,266.6461506,2174.062319,272.4569533,2146.598713,285.2529728,2533.425757,288.0896915 +5,0,0,2499.466773,379.0932172,2160.353152,385.9136581,2160.353152,399.0843419,2519.466773,400.9047828 +5,5,0,2513.425757,491.9083085,2146.598713,499.7450272,2174.062319,512.5410467,2505.553326,513.3518494 +5,10,0,2527.430502,605.0932341,2132.798779,613.9529195,2187.726438,625.6249339,2491.685194,625.4326892 +5,15,0,2541.481233,718.6498158,2118.953123,728.5392062,2201.34573,738.3378381,2477.862154,737.1490884 +10,-15,0,2556.148121,43.43543629,2302.286228,46.05779618,2220.528553,53.98090358,2660.359356,63.76027467 +10,-10,0,2570.069554,154.6601616,2288.772391,159.2756494,2234.268022,169.0890773,2646.209634,176.8089124 +10,-5,0,2584.0362,266.2461209,2275.213875,272.867823,2247.961914,283.8154183,2632.106017,289.489196 +10,0,0,2598.04828,378.195077,2261.610457,386.8361765,2261.610457,398.1618235,2618.04828,401.802923 +10,5,0,2612.106017,490.508804,2247.961914,501.1825817,2275.213875,512.130177,2604.0362,513.7518791 +10,10,0,2626.209634,603.1890876,2234.268022,615.9089227,2288.772391,625.7223506,2590.069554,625.3378384 +10,15,0,2640.359356,716.2377253,2220.528553,731.0170964,2302.286228,738.9402038,2576.148121,736.5625637 +15,-15,0,2653.57248,44.01681932,2404.135505,45.4500073,2323.027651,51.48048091,2758.362217,66.15101352 +15,-10,0,2667.590051,154.7541795,2390.729581,159.1773541,2336.658426,167.1153169,2744.115801,178.6962309 +15,-5,0,2681.652947,265.8496094,2377.279133,273.282404,2350.24378,282.3648351,2729.9156,290.8763523 +15,0,0,2695.76139,377.3048485,2363.783941,387.7670419,2363.783941,397.2309581,2715.76139,402.6931515 +15,5,0,2709.9156,489.1216477,2350.24378,502.6331649,2377.279133,511.715596,2701.652947,514.1483906 +15,10,0,2724.115801,601.3017691,2336.658426,617.8826831,2390.729581,625.8206459,2687.590051,625.2438205 +15,15,0,2738.362217,713.8469865,2323.027651,733.5175191,2404.135505,739.5479927,2673.57248,735.9811807 +20,-15,0,2750.146512,44.593128,2506.905888,44.8367217,2426.463072,48.95721703,2855.501387,68.52068297 +20,-10,0,2764.258014,154.8473755,2493.610388,159.0781684,2439.982592,165.1235567,2841.160524,180.5669435 +20,-5,0,2778.414946,265.4565698,2480.270531,273.700747,2453.456858,280.9010453,2826.865979,292.2513233 +20,0,0,2792.617527,376.4224275,2466.886096,388.706368,2466.886096,396.291632,2812.617527,403.5755725 +20,5,0,2806.865979,487.7466767,2453.456858,504.0969547,2480.270531,511.297253,2798.414946,514.5414302 +20,10,0,2821.160524,599.4310565,2439.982592,619.8744433,2493.610388,625.9198316,2784.258014,625.1506245 +20,15,0,2835.501387,711.477317,2426.463072,736.040783,2506.905888,740.1612783,2770.146512,735.404872 +25,-15,0,2845.881301,45.16442846,2610.60993,44.21786446,2530.847705,46.41079751,2951.788232,70.86956034 +25,-10,0,2860.084573,154.9397603,2597.427421,158.9780801,2544.253353,163.1135491,2937.355123,182.4212685 +25,-5,0,2874.333372,265.0669567,2584.200732,274.1229033,2557.613923,279.4238676,2922.968425,293.6142686 +25,0,0,2888.627916,375.5477119,2570.92964,389.6542708,2570.92964,395.3437292,2908.627916,404.4502881 +25,5,0,2902.968425,486.3837314,2557.613923,505.5741324,2584.200732,510.8750967,2894.333372,514.9310433 +25,10,0,2917.355123,597.5767315,2544.253353,621.8844509,2597.427421,626.0199199,2880.084573,625.0582397 +25,15,0,2931.788232,709.1284397,2530.847705,738.5872025,2610.60993,740.7801355,2865.881301,734.8335715 +30,-15,0,2940.787738,45.73078571,2715.260411,43.5933593,2636.194677,43.84090215,3047.233922,73.19791807 +30,-10,0,2955.080667,155.0313445,2702.193517,158.8770768,2649.483777,161.0850422,3032.71072,184.2594202 +30,-5,0,2969.41921,264.6807255,2689.082629,274.5489253,2662.727984,277.9331176,3018.234015,294.9653455 +30,0,0,2983.803586,374.6806011,2675.927527,390.6108683,2675.927527,394.3871317,3003.803586,405.3173989 +30,5,0,2998.234015,485.0326545,2662.727984,507.0648824,2689.082629,510.4490747,2989.41921,515.3172745 +30,10,0,3012.71072,595.7385798,2649.483777,623.9129578,2702.193517,626.1209232,2975.080667,624.9666555 +30,15,0,3027.233922,706.8000819,2636.194677,741.1570979,2715.260411,741.4046407,2960.787738,734.2672143 +35,-15,0,3034.876529,46.29226364,2820.870349,42.96312856,2742.517357,41.24720479,3141.849431,75.50602389 +35,-10,0,3049.257043,155.1221384,2807.92175,158.7751459,2755.687172,159.0377796,3127.238243,186.0816093 +35,-5,0,3063.683253,264.2978324,2794.929357,274.9788664,2768.812293,276.4286073,3112.673633,296.3047083 +35,0,0,3078.155376,373.8209963,2781.892946,391.5762806,2781.892946,393.4217194,3098.155376,406.1770037 +35,5,0,3092.673633,483.6932917,2768.812293,508.5693927,2794.929357,510.0191336,3083.683253,515.7001676 +35,10,0,3107.238243,593.9163907,2755.687172,625.9602204,2807.92175,626.2228541,3069.257043,624.8758616 +35,15,0,3121.849431,704.4919761,2742.517357,743.7507952,2820.870349,742.0348714,3054.876529,733.7057364 +40,-15,0,3128.158196,46.84892502,2927.452998,42.32709314,2849.829364,38.62937322,3235.645544,77.79414088 +40,-10,0,3142.624267,155.2121523,2914.625437,158.6722746,2862.877097,156.9714999,3220.948434,187.8880427 +40,-5,0,3157.136109,263.9182342,2901.754289,275.4127808,2875.880344,274.9101454,3206.297972,297.6325086 +40,0,0,3171.693937,372.9688006,2888.839333,392.5506301,2888.839333,392.4473699,3191.693937,407.0291994 +40,5,0,3186.297972,482.3654914,2875.880344,510.0878546,2901.754289,509.5852192,3177.136109,516.0797658 +40,10,0,3200.948434,592.1099573,2862.877097,628.0265001,2914.625437,626.3257254,3162.624267,624.7858477 +40,15,0,3215.645544,702.2038591,2849.829364,746.3686268,2927.452998,742.6709069,3148.158196,733.149075 +45,-15,0,3220.643078,47.40083156,3035.021858,41.68517246,2958.14457,35.98706902,3328.632859,80.06252757 +45,-10,0,3235.192723,155.301396,3022.318137,158.5684498,2971.06736,154.8859368,3313.851846,189.6789241 +45,-5,0,3249.788204,263.5418887,3009.571049,275.850724,2983.945886,273.3775369,3299.117547,298.9488956 +45,0,0,3264.429739,372.1239185,2996.780374,393.5340416,2996.780374,391.4639584,3284.429739,407.8740815 +45,5,0,3279.117547,481.0491044,2983.945886,511.6204631,3009.571049,509.147276,3269.788204,516.4561113 +45,10,0,3293.851846,590.3190759,2971.06736,630.1120632,3022.318137,626.4295502,3255.192723,624.696604 +45,15,0,3308.632859,699.9354724,2958.14457,749.010931,3035.021858,743.3128275,3240.643078,732.5971684 +50,-15,0,3312.341343,47.94804394,3143.590683,41.03728447,3067.477109,33.31994741,3420.821792,82.31143807 +50,-10,0,3326.972616,155.3898795,3131.013666,158.4636582,3080.272032,152.7808191,3405.958854,191.4544534 +50,-5,0,3341.649787,263.1687542,3118.393515,276.2927523,3093.022923,271.8305831,3391.142687,300.2540157 +50,0,0,3356.373071,371.2862563,3105.730009,394.526642,3105.730009,390.471358,3376.373071,408.7117437 +50,5,0,3371.142687,479.7439843,3093.022923,513.1674169,3118.393515,508.7052477,3361.649787,516.8292458 +50,10,0,3385.958854,588.5435466,3080.272032,632.2171809,3131.013666,626.5343418,3346.972616,624.6081205 +50,15,0,3400.821792,697.6865619,3067.477109,751.6780526,3143.590683,743.9607155,3332.341343,732.0499561 +55,-15,0,3403.262982,48.49062178,3253.17348,40.38334556,3177.841381,30.62765708,3512.222581,84.54112215 +55,-10,0,3417.973981,155.4776125,3240.726097,158.3578863,3190.505446,150.6558706,3497.279655,193.2148271 +55,-5,0,3432.730931,262.7987897,3228.235825,276.7389231,3203.125723,270.2690818,3482.38355,301.5480131 +55,0,0,3447.534049,370.4557219,3215.702441,395.5285607,3215.702441,389.4694393,3467.534049,409.5422781 +55,5,0,3462.38355,478.4499869,3203.125723,514.7289182,3228.235825,508.2590769,3452.730931,517.1992103 +55,10,0,3477.279655,586.7831729,3190.505446,634.3421294,3240.726097,626.6401137,3437.973981,624.5203875 +55,15,0,3492.222581,695.4568778,3177.841381,754.3703429,3253.17348,744.6146544,3423.262982,731.5073782 +60,-15,0,3493.417821,49.02862371,3363.784521,39.72327057,3289.252061,27.9098401,3602.845292,86.75182531 +60,-10,0,3508.206681,155.5646043,3351.469767,158.2511201,3301.782207,148.5108096,3587.824272,194.9602384 +60,-5,0,3523.04154,262.4319551,3339.112381,277.189295,3314.268825,268.6928266,3572.850119,302.8310292 +60,0,0,3537.922614,369.6322246,3326.712142,396.5399296,3326.712142,388.4580704,3557.922614,410.3657754 +60,5,0,3552.850119,477.1669708,3314.268825,516.3051734,3339.112381,507.808705,3543.04154,517.5660449 +60,10,0,3567.824272,585.0377616,3301.782207,636.4871904,3351.469767,626.7468799,3528.206681,624.4333957 +60,15,0,3582.845292,693.2461747,3289.252061,757.0881599,3363.784521,745.2747294,3513.417821,730.9693763 +65,-15,0,3582.815518,49.56210739,3475.438347,39.05697274,3401.7241,25.1661317,3692.699816,88.94378889 +65,-10,0,3597.680414,155.6508645,3463.259285,158.1433456,3414.117199,146.3453494,3677.602559,196.6908773 +65,-5,0,3612.591352,262.0682109,3451.037861,277.6439275,3426.467042,267.1016076,3662.552207,304.1032033 +65,0,0,3627.548544,368.8156754,3438.773855,397.5608831,3438.773855,387.4371169,3647.548544,411.1823246 +65,5,0,3642.552207,475.8947967,3426.467042,517.8963924,3451.037861,507.3540725,3632.591352,517.9297891 +65,10,0,3657.602559,583.3071227,3414.117199,638.6526506,3463.259285,626.8546544,3617.680414,624.3471355 +65,15,0,3672.699816,691.0542111,3401.7241,759.8318683,3475.438347,745.9410273,3602.815518,730.4358926 +70,-15,0,3671.465574,50.09112947,3588.149774,38.38436363,3515.272738,22.3961601,3781.795881,91.11725017 +70,-10,0,3686.404717,155.7364022,3576.109536,158.0345485,3527.52559,144.1591974,3766.624204,198.4069306 +70,-5,0,3701.389938,261.707518,3564.02722,278.1028813,3539.73547,265.4952106,3751.499466,305.3646723 +70,0,0,3716.421449,368.0059868,3551.902606,398.5915579,3551.902606,386.4064421,3736.421449,411.9920132 +70,5,0,3731.499466,474.6333277,3539.73547,519.5027894,3564.02722,506.8951187,3721.389938,518.290482 +70,10,0,3746.624204,581.5910694,3527.52559,640.8388026,3576.109536,626.9634515,3706.404717,624.2615978 +70,15,0,3761.795881,688.8807498,3515.272738,762.6018399,3588.149774,746.6136364,3691.465574,729.9068705 +75,-15,0,3759.377326,50.61574572,3701.933899,37.70535316,3629.913508,19.59954642,3870.143049,93.27244242 +75,-10,0,3774.388966,155.8212264,3690.035689,157.9247142,3642.022838,141.9520558,3854.898732,200.1085819 +75,-5,0,3789.446713,261.3498383,3678.095699,278.5662185,3654.089496,263.8734175,3839.701381,306.6155706 +75,0,0,3804.55078,367.2030726,3666.113709,399.6320936,3666.113709,385.3659064,3824.55078,412.7949274 +75,5,0,3819.701381,473.3824294,3654.089496,521.1245825,3678.095699,506.4317815,3809.446713,518.6481617 +75,10,0,3834.898732,579.8894181,3642.022838,643.0459442,3690.035689,627.0732858,3794.388966,624.1767736 +75,15,0,3850.143049,686.7255576,3629.913508,765.3984536,3701.933899,747.2926468,3779.377326,729.3822543 +80,-15,0,3846.55996,51.13601092,3816.80611,37.0198495,3745.662242,16.77590442,3957.750723,95.40959503 +80,-10,0,3861.642381,155.905346,3805.053205,157.8138277,3757.6247,139.7236207,3942.43551,201.7960117 +80,-5,0,3876.770934,260.9951342,3793.258832,279.0340021,3769.544801,262.2360058,3927.167284,307.8560307 +80,0,0,3891.94583,366.4068481,3781.422771,400.6826324,3781.422771,384.3153676,3911.94583,413.5911519 +80,5,0,3907.167284,472.1419693,3769.544801,522.7619942,3793.258832,505.9639979,3896.770934,519.0028658 +80,10,0,3922.43551,578.2019883,3757.6247,645.2743793,3805.053205,627.1841723,3881.642381,624.092654 +80,15,0,3937.750723,684.588405,3745.662242,768.2220956,3816.80611,747.9781505,3866.55996,728.8619891 +85,-15,0,3933.02251,51.651979,3932.78209,36.32775907,3862.53508,13.92484036,4044.628149,97.52893354 +85,-10,0,3948.174031,155.9887697,3921.177841,157.7018738,3874.347239,137.4735826,4029.243746,203.4693975 +85,-5,0,3963.371703,260.6433687,3909.532451,279.5062964,3886.117372,260.5827488,4013.906348,309.0861826 +85,0,0,3978.615738,365.6172302,3897.845702,401.7433193,3897.845702,383.2546807,3998.615738,414.3807698 +85,5,0,3993.906348,470.9118174,3886.117372,524.4152512,3909.532451,505.4917036,3983.371703,519.3546313 +85,10,0,4009.243746,576.5286025,3874.347239,647.5244174,3921.177841,627.2961262,3968.174031,624.0092303 +85,15,0,4024.628149,682.4690665,3862.53508,771.0731596,3932.78209,748.6702409,3953.02251,728.346021 +90,-15,0,4018.77386,52.16370297,4049.877823,35.62898647,3980.548477,11.04595284,4130.784419,99.63067976 +90,-10,0,4033.992835,156.0715062,4038.42566,157.5888371,3992.206831,135.2016257,4115.3325,205.128914 +90,-5,0,4049.257975,260.2945054,4026.932698,279.9831669,4003.823504,258.9134153,4099.927595,310.3061542 +90,0,0,4064.569492,364.8341369,4015.398719,402.814302,4015.398719,382.183698,4084.569492,415.1638631 +90,5,0,4079.927595,469.6918458,4003.823504,526.0845847,4026.932698,505.0148331,4069.257975,519.7034946 +90,10,0,4095.3325,574.869086,3992.206831,649.7963743,4038.42566,627.4091629,4053.992835,623.9264938 +90,15,0,4110.784419,680.3673202,3980.548477,773.9520472,4049.877823,749.3690135,4038.77386,727.834297 +95,-15,0,4103.822749,52.67123498,4168.109606,34.92343446,4099.719212,8.138832551,4216.228476,101.7150518 +95,-10,0,4119.107565,156.153564,4156.813036,157.4747017,4111.220172,132.9074282,4200.710678,206.7747329 +95,-5,0,4134.438555,259.9485086,4145.476025,280.4646806,4122.679813,257.2277698,4185.239901,311.5160715 +95,0,0,4149.81593,364.0574877,4134.098357,403.8957311,4134.098357,381.1022689,4169.81593,415.9405123 +95,5,0,4165.239901,468.4819285,4122.679813,527.7702302,4145.476025,504.5333194,4154.438555,520.0494914 +95,10,0,4180.710678,573.2232671,4111.220172,652.0905718,4156.813036,627.5232983,4139.107565,623.844436 +95,15,0,4196.228476,678.2829482,4099.719212,776.8591674,4168.109606,750.0745655,4123.822749,727.326765 +100,-15,0,4188.177773,53.17462633,4287.494052,34.21100389,4220.064393,5.203062151,4300.969115,103.7822643 +100,-10,0,4203.52685,156.2349512,4276.356663,157.3594517,4231.404287,130.5906619,4285.387042,208.4070231 +100,-5,0,4218.922106,259.6053431,4265.17921,280.9509055,4242.703241,255.525572,4269.851991,312.7160582 +100,0,0,4234.36375,363.2872033,4253.961475,404.9877603,4253.961475,380.0102397,4254.36375,416.7107967 +100,5,0,4249.851991,467.2819418,4242.703241,529.472428,4265.17921,504.0470945,4238.922106,520.3926569 +100,10,0,4265.387042,571.5909769,4231.404287,654.4073381,4276.356663,627.6385483,4223.52685,623.7630488 +100,15,0,4280.969115,676.2157357,4220.064393,779.7949378,4287.494052,750.7869961,4208.177773,726.8233737 +105,-15,0,4271.847389,53.67392749,4408.048099,33.4915937,4341.601466,2.23821601,4385.014986,105.8325282 +105,-10,0,4287.259178,156.3156762,4397.073565,157.2430704,4352.776537,128.2509921,4369.370211,210.0259506 +105,-5,0,4302.717147,259.2649742,4386.059358,281.4419112,4363.911062,253.8065769,4353.772453,313.9062361 +105,0,0,4318.221502,362.5232059,4375.005263,406.0905461,4375.005263,378.9074539,4338.221502,417.4747941 +105,5,0,4333.772453,466.0917639,4363.911062,531.1914231,4386.059358,503.5560888,4322.717147,520.7330258 +105,10,0,4349.370211,569.9720494,4352.776537,656.7470079,4397.073565,627.7549296,4307.259178,623.6823238 +105,15,0,4365.014986,674.1654718,4341.601466,782.759784,4408.048099,751.5064063,4291.847389,726.3240725 +-105,-15,5,101.045586,-2.66565053,124.7049792,32.99774544,30.45280971,128.3613759,188.469832,28.8009483 +-105,-10,5,112.1809233,121.8372693,109.114415,136.1559223,46.28035033,233.0875561,177.129674,155.5939439 +-105,-5,5,123.3567784,246.7932137,93.47688323,239.6248699,62.06006538,337.4972885,165.8309298,281.9238972 +-105,0,5,134.5733729,372.2046598,77.7921713,343.4059947,77.7921713,441.5920053,154.5733729,407.7933402 +-105,5,5,145.8309298,498.0741028,62.06006538,447.5007115,93.47688323,545.3731301,143.3567784,533.2047863 +-105,10,5,157.129674,624.4040561,46.28035033,551.9104439,109.114415,648.8420777,132.1809233,658.1607307 +-105,15,5,168.469832,751.1970517,30.45280971,656.6366241,124.7049792,752.0002546,121.045586,782.6636505 +-100,-15,5,222.1406602,-1.788710736,208.5679809,32.39043087,114.6914016,126.3931365,310.5554493,31.65348557 +-100,-10,5,233.4400979,122.1038533,193.0400276,135.9714149,130.4563594,231.5550863,299.0492071,157.8135549 +-100,-5,5,244.780448,246.4450013,177.4651029,239.8657269,146.1734842,336.3979605,287.5847776,283.5151702 +-100,0,5,256.1619332,371.237174,161.8429934,344.074791,161.8429934,440.923209,276.1619332,408.760826 +-100,5,5,267.5847776,496.4828298,146.1734842,448.6000395,177.4651029,545.1322731,264.780448,533.5529987 +-100,10,5,279.0492071,622.1844451,130.4563594,553.4429137,193.0400276,649.0265851,253.4400979,657.8941467 +-100,15,5,290.5554493,748.3445144,114.6914016,658.6048635,208.5679809,752.6075691,242.1406602,781.7867107 +-95,-15,5,342.0533016,-0.920333821,293.1217145,31.77811419,199.6301799,124.4085372,431.4359824,34.47786599 +-95,-10,5,353.513108,122.3678296,277.657936,135.7853854,215.3309673,230.0098982,419.7674753,160.0112962 +-95,-5,5,365.0142043,246.1002013,262.1471871,240.1085738,230.983919,335.2895228,408.1411619,285.0907928 +-95,0,5,376.5568138,370.2791863,246.5892533,344.7491209,246.5892533,440.2488791,396.5568138,409.7188137 +-95,5,5,388.1411619,494.9072072,230.983919,449.7084772,262.1471871,544.8894262,385.0142043,533.8977987 +-95,10,5,399.7674753,619.9867038,215.3309673,554.9881018,277.657936,649.2126146,373.513108,657.6301704 +-95,15,5,411.4359824,745.520134,199.6301799,660.5894628,293.1217145,753.2198858,362.0533016,780.9183338 +-90,-15,5,460.8007446,-0.060394976,378.3747491,31.16073335,285.2779111,122.4073732,551.1291866,37.2745044 +-90,-10,5,472.4172736,122.6292362,362.9767418,135.597815,300.9129071,228.4518327,539.3021455,162.1874895 +-90,-5,5,484.075453,245.7587637,347.5317699,240.3534352,316.5000697,334.1718617,527.5176621,286.6509949 +-90,0,5,495.7755072,369.3305576,332.039618,345.4290535,332.039618,439.5689465,515.7755072,410.6674424 +-90,5,5,507.5176621,493.3470051,316.5000697,450.8261383,347.5317699,544.6445648,504.075453,534.2392363 +-90,10,5,519.3021455,617.8105105,300.9129071,556.5461673,362.9767418,649.400185,492.4172736,657.3687638 +-90,15,5,531.1291866,742.7234956,285.2779111,662.5906268,378.3747491,753.8372667,480.8007446,780.058395 +-85,-15,5,578.3998908,0.791228194,464.335796,30.53822526,371.6435082,120.3894362,669.6524698,40.04380754 +-85,-10,5,590.1695792,122.8881104,449.0051896,135.4086845,387.2110579,226.8807283,657.6705399,164.3424501 +-85,-5,5,601.9812623,245.4206396,433.6276293,240.6003364,402.7307815,333.0448615,645.7315151,288.1960016 +-85,0,5,613.8351654,368.3911514,418.202899,346.1146587,418.202899,438.8833413,633.8351654,411.6068486 +-85,5,5,625.7315151,491.8019984,402.7307815,451.9531385,433.6276293,544.3976636,621.9812623,534.5773604 +-85,10,5,637.6705399,615.6555499,387.2110579,558.1172717,449.0051896,649.5893155,610.1695792,657.1098896 +-85,15,5,649.6524698,739.9541925,371.6435082,664.6085638,464.335796,754.4597747,598.3998908,779.2067718 +-80,-15,5,694.8673161,1.634655729,551.0137119,29.91052579,458.7360347,118.3545144,787.0229014,42.78617424 +-80,-10,5,706.7866816,123.144489,535.75217,135.2179743,474.2344482,225.2964204,774.8896443,166.4764872 +-80,-5,5,718.7483705,245.085781,520.4436902,240.8493029,489.6850482,331.9084048,762.7996237,289.7260339 +-80,0,5,730.7526087,367.4608339,505.0880557,346.8060079,505.0880557,438.1919921,750.7526087,412.5371661 +-80,5,5,742.7996237,490.2719661,489.6850482,453.0895952,520.4436902,544.1486971,738.7483705,534.912219 +-80,10,5,754.8896443,613.5215128,474.2344482,559.7015796,535.75217,649.7800257,726.7866816,656.853511 +-80,15,5,767.0229014,737.2118258,458.7360347,666.6434856,551.0137119,755.0874742,714.8673161,778.3633443 +-75,-15,5,810.219279,2.470005368,638.4175018,29.27756973,546.5647071,116.3023926,903.2572204,45.50199561 +-75,-10,5,822.2849182,123.3984078,623.2267229,135.0256646,561.9922588,223.698742,890.9761163,168.5899042 +-75,-5,5,834.3931943,244.7541409,607.9890275,241.1003608,577.3720151,330.762372,878.7385645,291.2413085 +-75,0,5,846.5443337,366.5394737,592.7041979,347.5031737,592.7041979,437.4948263,866.5443337,413.4585263 +-75,5,5,858.7385645,488.7566915,577.3720151,454.235628,607.9890275,543.8976392,854.3931943,535.2438591 +-75,10,5,870.9761163,611.4080958,561.9922588,561.299258,623.2267229,649.9723354,842.2849182,656.5995922 +-75,15,5,883.2572204,734.4960044,546.5647071,668.6956074,638.4175018,755.7204303,830.219279,777.5279946 +-70,-15,5,924.4717281,3.297392607,726.5563222,28.63929076,635.1388982,114.2328516,1018.371843,48.1916552 +-70,-10,5,936.6803142,123.649902,711.4380403,134.8317352,650.4938261,222.0875232,1005.946293,170.6829984 +-70,-5,5,948.9318361,244.425673,696.2728689,241.3535366,665.8009823,329.6066416,993.5645962,292.7420377 +-70,0,5,961.2265206,365.6269422,681.0605895,348.2062296,681.0605895,436.7917704,981.2265206,414.3710578 +-70,5,5,973.5645962,487.2559623,665.8009823,455.3913584,696.2728689,543.6444634,968.9318361,535.572327 +-70,10,5,985.9462931,609.3150016,650.4938261,562.9104768,711.4380403,650.1662648,956.6803142,656.348098 +-70,15,5,998.3718431,731.8063448,635.1388982,670.7651484,726.5563222,756.3587092,944.4717281,776.7006074 +-65,-15,5,1037.640309,4.116930751,815.4394841,27.99562146,724.4681409,112.1456689,1132.382871,50.85552925 +-65,-10,5,1049.98859,123.8990062,800.3954699,134.6361654,739.7486455,220.4625909,1119.816199,172.7560616 +-65,-5,5,1062.380092,244.100332,785.3045988,241.6088571,754.981408,328.4410899,1107.293667,294.2284302 +-65,0,5,1074.815041,364.723113,770.1666517,348.9152507,770.1666517,436.0827493,1094.815041,415.274887 +-65,5,5,1087.293667,485.7695698,754.981408,456.5569101,785.3045988,543.3891429,1082.380092,535.897668 +-65,10,5,1099.816199,607.2419384,739.7486455,564.5354091,800.3954699,650.3618346,1069.98859,656.0989938 +-65,15,5,1112.382871,729.1424707,724.4681409,672.8523311,815.4394841,757.0023785,1057.640309,775.8810692 +-60,-15,5,1149.740373,4.928730964,905.0764567,27.34649326,814.5621312,110.0406178,1245.306099,53.49398679 +-60,-10,5,1162.22517,124.1457544,890.108518,134.4389345,829.7663747,218.8237695,1232.601554,174.8093799 +-60,-5,5,1174.753458,243.7780736,875.0937612,241.8663497,844.9229116,327.2655912,1219.94142,295.7006903 +-60,0,5,1187.325465,363.8278624,860.0319661,349.6303132,860.0319661,435.3676868,1207.325465,416.1701376 +-60,5,5,1199.94142,484.2973097,844.9229116,457.7324088,875.0937612,543.1316503,1194.753458,536.2199264 +-60,10,5,1212.601554,605.1886201,829.7663747,566.1742305,890.108518,650.5590655,1182.22517,655.8522456 +-60,15,5,1225.306099,726.5040132,814.5621312,674.9573822,905.0764567,757.6515067,1169.740373,775.069269 +-55,-15,5,1260.786982,5.73290232,995.4768702,26.69183641,905.4307318,107.9174678,1357.157021,56.10738984 +-55,-10,5,1273.405187,124.3901797,980.5868533,134.2400211,920.5568371,217.1708801,1344.317778,176.8432342 +-55,-5,5,1286.06714,243.4588541,965.6500629,242.1260424,935.6352776,326.0800177,1331.523205,297.1590186 +-55,0,5,1298.77307,362.9410686,950.6662784,350.3514946,950.6662784,434.6465054,1318.77307,417.0569314 +-55,5,5,1311.523205,482.8389814,935.6352776,458.9179823,965.6500629,542.8719576,1306.06714,536.5391459 +-55,10,5,1324.317778,603.1547658,920.5568371,567.8271199,980.5868533,650.7579789,1293.405187,655.6078203 +-55,15,5,1337.157021,723.8906102,905.4307318,677.0805322,995.4768702,758.3061636,1280.786982,774.2650977 +-50,-15,5,1370.794916,6.529551856,1086.65052,26.03157997,997.0839758,105.7759847,1467.950839,58.69609359 +-50,-10,5,1383.543489,124.6323149,1071.84031,134.0394036,1012.130026,215.5037407,1454.980003,178.8578998 +-50,-5,5,1396.336055,243.1426307,1056.983378,242.3879632,1027.128459,324.8842392,1442.054082,298.6036119 +-50,0,5,1409.172843,362.0626124,1042.079502,351.0788739,1042.079502,433.9191261,1429.172843,417.9353876 +-50,5,5,1422.054082,481.3943881,1027.128459,460.1137608,1056.983378,542.6100368,1416.336055,536.8553693 +-50,10,5,1434.980003,601.1401002,1012.130026,569.4942593,1071.84031,650.9585964,1403.543489,655.3656851 +-50,15,5,1447.950839,721.3019064,997.0839758,679.2220153,1086.65052,758.96642,1390.794916,773.4684481 +-45,-15,5,1479.77868,7.318784613,1178.607369,25.36565181,1089.53207,103.6159298,1577.702468,61.26044654 +-45,-10,5,1492.65465,124.8721919,1163.878891,133.83706,1104.496106,213.8221663,1564.603074,180.853647 +-45,-5,5,1505.574845,242.8293614,1149.103749,242.6521412,1119.412581,323.6781235,1551.548827,300.0346635 +-45,0,5,1518.539494,361.1923769,1134.28172,351.8125313,1134.28172,433.1854687,1538.539494,418.8056231 +-45,5,5,1531.548827,479.9633365,1119.412581,461.3198765,1149.103749,542.3458588,1525.574845,537.1686386 +-45,10,5,1544.603074,599.144353,1104.496106,571.1758337,1163.878891,651.16094,1512.65465,655.1258081 +-45,15,5,1557.702468,718.7375535,1089.53207,681.3820702,1178.607369,759.6323482,1499.77868,772.6792154 +-40,-15,5,1587.752509,8.100703686,1271.357553,24.69397852,1182.7854,101.4370605,1686.426545,63.80079068 +-40,-10,5,1600.752971,125.1098422,1256.712774,133.632968,1197.665422,212.1259685,1673.201559,182.8307412 +-40,-5,5,1613.797877,242.5190051,1242.021395,242.9186055,1212.497944,322.4615359,1660.021941,301.4523627 +-40,0,5,1626.887457,360.3302471,1227.283193,352.5525483,1227.283193,432.4454517,1646.887457,419.6677529 +-40,5,5,1640.021941,478.5456373,1212.497944,462.5364641,1242.021395,542.0793945,1633.797877,537.4789949 +-40,10,5,1653.201559,597.1672588,1197.665422,572.8720315,1256.712774,651.365032,1620.752971,654.8881578 +-40,15,5,1666.426545,716.1972093,1182.7854,683.5609395,1271.357553,760.3040215,1607.752509,771.8972963 +-35,-15,5,1694.730379,8.875410268,1364.911382,24.01648543,1276.854531,99.23912998,1794.137433,66.31746161 +-35,-10,5,1707.852489,125.3452967,1350.352311,133.4271048,1291.648495,210.4149559,1780.789757,184.7894424 +-35,-5,5,1721.019253,242.2115212,1335.746711,243.1873861,1306.395028,321.2343394,1767.487656,302.8568957 +-35,0,5,1734.230899,359.4761103,1321.094358,353.2990081,1321.094358,431.6989919,1754.230899,420.5218897 +-35,5,5,1747.487656,477.1411043,1306.395028,463.7636606,1335.746711,541.8106139,1741.019253,537.7864788 +-35,10,5,1760.789757,595.2085576,1291.648495,574.5830441,1350.352311,651.5708952,1727.852489,654.6527033 +-35,15,5,1774.137433,713.6805384,1276.854531,685.75887,1364.911382,760.9815146,1714.730379,771.1225897 +-30,-15,5,1800.726006,9.643003694,1459.279347,23.33309658,1371.750217,97.02188698,1900.849229,68.81078869 +-30,-10,5,1813.966985,125.5785857,1444.808035,133.2194472,1386.456035,208.6889334,1887.381699,186.7300064 +-30,-5,5,1827.252815,241.9068702,1430.290272,243.4585131,1401.114499,319.9963946,1873.959942,304.2484449 +-30,0,5,1840.583724,358.6298559,1415.725835,354.0519952,1415.725835,430.9460048,1860.583724,421.3681441 +-30,5,5,1853.959942,475.7495551,1401.114499,465.0016054,1430.290272,541.5394869,1847.252815,538.0911298 +-30,10,5,1867.381699,593.2679936,1386.456035,576.3090666,1444.808035,651.7785528,1833.966985,654.4194143 +-30,15,5,1880.849229,711.1872113,1371.750217,687.976113,1459.279347,761.6649034,1820.726006,770.3549963 +-25,-15,5,1905.752855,10.40358148,1554.472123,22.64373466,1467.483399,94.78507583,2006.57577,71.28109524 +-25,-10,5,1919.109986,125.8097388,1540.090665,133.0099718,1482.098938,206.9477026,1992.991161,188.6526837 +-25,-5,5,1932.512152,241.605013,1525.662843,243.7320176,1496.667206,318.7475598,1979.45251,305.6271896 +-25,0,5,1945.959584,357.7913753,1511.188432,354.8115955,1511.188432,430.1864045,1965.959584,422.2066247 +-25,5,5,1959.45251,474.3708104,1496.667206,466.2504402,1525.662843,541.2659824,1952.512152,538.392987 +-25,10,5,1972.991161,591.3453163,1482.098938,578.0502974,1540.090665,651.9880282,1939.109986,654.1882612 +-25,15,5,1986.57577,708.7169048,1467.483399,690.2129242,1554.472123,762.3542653,1925.752855,769.5944185 +-20,-15,5,2009.824149,11.15723938,1650.500569,21.94832102,1564.065213,92.52843634,2111.330639,73.72869859 +-20,-10,5,2023.294771,126.0387854,1636.211107,132.7986544,1578.588294,205.1910618,2097.631663,190.5577206 +-20,-5,5,2036.810604,241.3059114,1621.875374,244.0079308,1593.064195,317.4876906,2083.97882,306.9933058 +-20,0,5,2050.371877,356.9605618,1607.493145,355.5778966,1607.493145,429.4201034,2070.371877,423.0374382 +-20,5,5,2063.97882,473.0046942,1593.064195,467.5103094,1621.875374,540.9900692,2056.810604,538.6920886 +-20,10,5,2077.631663,589.4402794,1578.588294,579.8069382,1636.211107,652.1993456,2043.294771,653.9592146 +-20,15,5,2091.330639,706.2693014,1564.065213,692.4695637,1650.500569,763.049679,2029.824149,768.8407606 +-15,-15,5,2112.952869,11.90407139,1747.375739,21.24677563,1661.506994,90.25170368,2215.127168,76.15391031 +-15,-10,5,2126.53438,126.265754,1733.180459,132.5854707,1675.935389,203.4188053,2201.316479,192.4453587 +-15,-5,5,2140.161268,241.0095278,1718.939011,244.2862848,1690.316703,316.2166401,2187.552088,308.346966 +-15,0,5,2153.833761,356.1373108,1704.651168,356.3509875,1704.651168,428.6470125,2173.833761,423.8606892 +-15,5,5,2167.552088,471.651034,1690.316703,468.7813599,1718.939011,540.7117152,2160.161268,538.9884722 +-15,10,5,2181.316479,587.5526413,1675.935389,581.5791947,1733.180459,652.4125293,2146.53438,653.732246 +-15,15,5,2195.127168,703.8440897,1661.506994,694.7462963,1747.375739,763.7512244,2132.952869,768.0939286 +-10,-15,5,2215.151762,12.64416984,1845.108881,20.53901702,1759.820278,87.95460829,2317.97845,78.55703626 +-10,-10,5,2228.841618,126.4906729,1831.010018,132.3703959,1774.151711,201.630724,2304.058644,194.3158354 +-10,-5,5,2242.577005,240.7158253,1816.865097,244.5671121,1788.436172,314.9342587,2290.185289,309.68834 +-10,0,5,2256.358153,355.3215194,1802.673891,357.1309589,1802.673891,427.8670411,2276.358153,424.6764806 +-10,5,5,2270.185289,470.30966,1788.436172,470.0637413,1816.865097,540.4308879,2262.577005,539.2821747 +-10,10,5,2284.058644,585.6821646,1774.151711,583.367276,1831.010018,652.6276041,2248.841618,653.5073271 +-10,15,5,2297.97845,701.4409637,1759.820278,697.0433917,1845.108881,764.458983,2235.151762,767.3538302 +-5,-15,5,2316.433347,13.37762538,1943.711443,19.82496229,1859.016811,85.63687576,2419.897337,80.93837676 +-5,-10,5,2330.229058,126.7135696,1929.711282,132.1534046,1873.248956,199.8266051,2405.870952,196.1693835 +-5,-5,5,2344.070445,240.4247678,1915.665179,244.8504458,1887.434245,313.6403944,2391.891163,311.0175942 +-5,0,5,2357.957737,354.5130868,1901.57291,357.917903,1901.57291,427.080097,2377.957737,425.4849132 +-5,5,5,2371.891163,468.9804058,1887.434245,471.3576056,1915.665179,540.1475542,2364.070445,539.5732322 +-5,10,5,2385.870952,583.8286165,1873.248956,585.1713949,1929.711282,652.8445954,2350.229058,653.2844304 +-5,15,5,2399.897337,699.0596232,1859.016811,699.3611242,1943.711443,765.1730377,2336.433347,766.6203746 +0,-15,5,2416.80992,14.10452704,2043.19508,19.10452704,1959.108548,83.29822672,2520.896452,83.29822672 +0,-10,5,2430.709048,126.9344713,2029.295952,131.9344713,1973.239028,198.0062318,2506.765972,198.0062318 +0,-5,5,2444.65399,240.1363196,2015.35101,245.1363196,1987.322777,312.3348921,2492.682223,312.3348921 +0,0,5,2458.644972,353.7119138,2001.360028,358.7119138,2001.360028,426.2860862,2478.644972,426.2860862 +0,5,5,2472.682223,467.6631079,1987.322777,472.6631079,2015.35101,539.8616804,2464.65399,539.8616804 +0,10,5,2486.765972,581.9917682,1973.239028,586.9917682,2029.295952,653.0635287,2450.709048,653.0635287 +0,15,5,2500.896452,696.6997733,1959.108548,701.6997733,2043.19508,765.893473,2436.80992,765.893473 +5,-15,5,2516.293557,14.82496229,2143.571653,18.37762538,2060.107663,80.93837676,2620.988189,85.63687576 +5,-10,5,2530.293718,127.1534046,2129.775942,131.7135696,2074.134048,196.1693835,2606.756044,199.8266051 +5,-5,5,2544.339821,239.8504458,2115.934555,245.4247678,2088.113837,311.0175942,2592.570755,313.6403944 +5,0,5,2558.43209,352.917903,2102.047263,359.5130868,2102.047263,425.4849132,2578.43209,427.080097 +5,5,5,2572.570755,466.3576056,2088.113837,473.9804058,2115.934555,539.5732322,2564.339821,540.1475542 +5,10,5,2586.756044,580.1713949,2074.134048,588.8286165,2129.775942,653.2844304,2550.293718,652.8445954 +5,15,5,2600.988189,694.3611242,2060.107663,704.0596232,2143.571653,766.6203746,2536.293557,765.1730377 +10,-15,5,2614.896119,15.53901702,2244.853238,17.64416984,2162.02655,78.55703626,2720.184722,87.95460829 +10,-10,5,2628.994982,127.3703959,2231.163382,131.4906729,2175.946356,194.3158354,2705.853289,201.630724 +10,-5,5,2643.139903,239.5671121,2217.427995,245.7158253,2189.819711,309.68834,2691.568828,314.9342587 +10,0,5,2657.331109,352.1309589,2203.646847,360.3215194,2203.646847,424.6764806,2677.331109,427.8670411 +10,5,5,2671.568828,465.0637413,2189.819711,475.30966,2217.427995,539.2821747,2663.139903,540.4308879 +10,10,5,2685.853289,578.367276,2175.946356,590.6821646,2231.163382,653.5073271,2648.994982,652.6276041 +10,15,5,2700.184722,692.0433917,2162.02655,706.4409637,2244.853238,767.3538302,2634.896119,764.458983 +15,-15,5,2712.629261,16.24677563,2347.052131,16.90407139,2264.877832,76.15391031,2818.498006,90.25170368 +15,-10,5,2726.824541,127.5854707,2333.47062,131.265754,2278.688521,192.4453587,2804.069611,203.4188053 +15,-5,5,2741.065989,239.2862848,2319.843732,246.0095278,2292.452912,308.346966,2789.688297,316.2166401 +15,0,5,2755.353832,351.3509875,2306.171239,361.1373108,2306.171239,423.8606892,2775.353832,428.6470125 +15,5,5,2769.688297,463.7813599,2292.452912,476.651034,2319.843732,538.9884722,2761.065989,540.7117152 +15,10,5,2784.069611,576.5791947,2278.688521,592.5526413,2333.47062,653.732246,2746.824541,652.4125293 +15,15,5,2798.498006,689.7462963,2264.877832,708.8440897,2347.052131,768.0939286,2732.629261,763.7512244 +20,-15,5,2809.504431,16.94832102,2450.180851,16.15723938,2368.674361,73.72869859,2915.939787,92.52843634 +20,-10,5,2823.793893,127.7986544,2436.710229,131.0387854,2382.373337,190.5577206,2901.416706,205.1910618 +20,-5,5,2838.129626,239.0079308,2423.194396,246.3059114,2396.02618,306.9933058,2886.940805,317.4876906 +20,0,5,2852.511855,350.5778966,2409.633123,361.9605618,2409.633123,423.0374382,2872.511855,429.4201034 +20,5,5,2866.940805,462.5103094,2396.02618,478.0046942,2423.194396,538.6920886,2858.129626,540.9900692 +20,10,5,2881.416706,574.8069382,2382.373337,594.4402794,2436.710229,653.9592146,2843.793893,652.1993456 +20,15,5,2895.939787,687.4695637,2368.674361,711.2693014,2450.180851,768.8407606,2829.504431,763.049679 +25,-15,5,2905.532877,17.64373466,2554.252145,15.40358148,2473.42923,71.28109524,3012.521601,94.78507583 +25,-10,5,2919.914335,128.0099718,2540.895014,130.8097388,2487.013839,188.6526837,2997.906062,206.9477026 +25,-5,5,2934.342157,238.7320176,2527.492848,246.605013,2500.55249,305.6271896,2983.337794,318.7475598 +25,0,5,2948.816568,349.8115955,2514.045416,362.7913753,2514.045416,422.2066247,2968.816568,430.1864045 +25,5,5,2963.337794,461.2504402,2500.55249,479.3708104,2527.492848,538.392987,2954.342157,541.2659824 +25,10,5,2977.906062,573.0502974,2487.013839,596.3453163,2540.895014,654.1882612,2939.914335,651.9880282 +25,15,5,2992.521601,685.2129242,2473.42923,713.7169048,2554.252145,769.5944185,2925.532877,762.3542653 +30,-15,5,3000.725653,18.33309658,2659.278994,14.64300369,2579.155771,68.81078869,3108.254783,97.02188698 +30,-10,5,3015.196965,128.2194472,2646.038015,130.5785857,2592.623301,186.7300064,3093.548965,208.6889334 +30,-5,5,3029.714728,238.4585131,2632.752185,246.9068702,2606.045058,304.2484449,3078.890501,319.9963946 +30,0,5,3044.279165,349.0519952,2619.421276,363.6298559,2619.421276,421.3681441,3064.279165,430.9460048 +30,5,5,3058.890501,460.0016054,2606.045058,480.7495551,2632.752185,538.0911298,3049.714728,541.5394869 +30,10,5,3073.548965,571.3090666,2592.623301,598.2679936,2646.038015,654.4194143,3035.196965,651.7785528 +30,15,5,3088.254783,682.976113,2579.155771,716.1872113,2659.278994,770.3549963,3020.725653,761.6649034 +35,-15,5,3095.093618,19.01648543,2765.274621,13.87541027,2685.867567,66.31746161,3203.150469,99.23912998 +35,-10,5,3109.652689,128.4271048,2752.152511,130.3452967,2699.215243,184.7894424,3188.356505,210.4149559 +35,-5,5,3124.258289,238.1873861,2738.985747,247.2115212,2712.517344,302.8568957,3173.609972,321.2343394 +35,0,5,3138.910642,348.2990081,2725.774101,364.4761103,2725.774101,420.5218897,3158.910642,431.6989919 +35,5,5,3153.609972,458.7636606,2712.517344,482.1411043,2738.985747,537.7864788,3144.258289,541.8106139 +35,10,5,3168.356505,569.5830441,2699.215243,600.2085576,2752.152511,654.6527033,3129.652689,651.5708952 +35,15,5,3183.150469,680.75887,2685.867567,718.6805384,2765.274621,771.1225897,3115.093618,760.9815146 +40,-15,5,3188.647447,19.69397852,2872.252491,13.10070369,2793.578455,63.80079068,3297.2196,101.4370605 +40,-10,5,3203.292226,128.632968,2859.252029,130.1098422,2806.803441,182.8307412,3282.339578,212.1259685 +40,-5,5,3217.983605,237.9186055,2846.207123,247.5190051,2819.983059,301.4523627,3267.507056,322.4615359 +40,0,5,3232.721807,347.5525483,2833.117543,365.3302471,2833.117543,419.6677529,3252.721807,432.4454517 +40,5,5,3247.507056,457.5364641,2819.983059,483.5456373,2846.207123,537.4789949,3237.983605,542.0793945 +40,10,5,3262.339578,567.8720315,2806.803441,602.1672588,2859.252029,654.8881578,3223.292226,651.365032 +40,15,5,3277.2196,678.5609395,2793.578455,721.1972093,2872.252491,771.8972963,3208.647447,760.3040215 +45,-15,5,3281.397631,20.36565181,2980.22632,12.31878461,2902.302532,61.26044654,3390.47293,103.6159298 +45,-10,5,3296.126109,128.83706,2967.35035,129.8721919,2915.401926,180.853647,3375.508894,213.8221663 +45,-5,5,3310.901251,237.6521412,2954.430155,247.8293614,2928.456173,300.0346635,3360.592419,323.6781235 +45,0,5,3325.72328,346.8125313,2941.465506,366.1923769,2941.465506,418.8056231,3345.72328,433.1854687 +45,5,5,3340.592419,456.3198765,2928.456173,484.9633365,2954.430155,537.1686386,3330.901251,542.3458588 +45,10,5,3355.508894,566.1758337,2915.401926,604.144353,2967.35035,655.1258081,3316.126109,651.16094 +45,15,5,3370.47293,676.3820702,2902.302532,723.7375535,2980.22632,772.6792154,3301.397631,759.6323482 +50,-15,5,3373.35448,21.03157997,3089.210084,11.52955186,3012.054161,58.69609359,3482.921024,105.7759847 +50,-10,5,3388.16469,129.0394036,3076.461511,129.6323149,3025.024997,178.8578998,3467.874974,215.5037407 +50,-5,5,3403.021622,237.3879632,3063.668945,248.1426307,3037.950918,298.6036119,3452.876541,324.8842392 +50,0,5,3417.925498,346.0788739,3050.832157,367.0626124,3050.832157,417.9353876,3437.925498,433.9191261 +50,5,5,3432.876541,455.1137608,3037.950918,486.3943881,3063.668945,536.8553693,3423.021622,542.6100368 +50,10,5,3447.874974,564.4942593,3025.024997,606.1401002,3076.461511,655.3656851,3408.16469,650.9585964 +50,15,5,3462.921024,674.2220153,3012.054161,726.3019064,3089.210084,773.4684481,3393.35448,758.96642 +55,-15,5,3464.52813,21.69183641,3199.218018,10.73290232,3122.847979,56.10738984,3574.574268,107.9174678 +55,-10,5,3479.418147,129.2400211,3186.599813,129.3901797,3135.687222,176.8432342,3559.448163,217.1708801 +55,-5,5,3494.354937,237.1260424,3173.93786,248.4588541,3148.481795,297.1590186,3544.369722,326.0800177 +55,0,5,3509.338722,345.3514946,3161.23193,367.9410686,3161.23193,417.0569314,3529.338722,434.6465054 +55,5,5,3524.369722,453.9179823,3148.481795,487.8389814,3173.93786,536.5391459,3514.354937,542.8719576 +55,10,5,3539.448163,562.8271199,3135.687222,608.1547658,3186.599813,655.6078203,3499.418147,650.7579789 +55,15,5,3554.574268,672.0805322,3122.847979,728.8906102,3199.218018,774.2650977,3484.52813,758.3061636 +60,-15,5,3554.928543,22.34649326,3310.264627,9.928730964,3234.698901,53.49398679,3665.442869,110.0406178 +60,-10,5,3569.896482,129.4389345,3297.77983,129.1457544,3247.403446,174.8093799,3650.238625,218.8237695 +60,-5,5,3584.911239,236.8663497,3285.251542,248.7780736,3260.06358,295.7006903,3635.082088,327.2655912 +60,0,5,3599.973034,344.6303132,3272.679535,368.8278624,3272.679535,416.1701376,3619.973034,435.3676868 +60,5,5,3615.082088,452.7324088,3260.06358,489.2973097,3285.251542,536.2199264,3604.911239,543.1316503 +60,10,5,3630.238625,561.1742305,3247.403446,610.1886201,3297.77983,655.8522456,3589.896482,650.5590655 +60,15,5,3645.442869,669.9573822,3234.698901,731.5040132,3310.264627,775.069269,3574.928543,757.6515067 +65,-15,5,3644.565516,22.99562146,3422.364691,9.116930751,3347.622129,50.85552925,3755.536859,112.1456689 +65,-10,5,3659.60953,129.6361654,3410.01641,128.8990062,3360.188801,172.7560616,3740.256355,220.4625909 +65,-5,5,3674.700401,236.6088571,3397.624908,249.100332,3372.711333,294.2284302,3725.023592,328.4410899 +65,0,5,3689.838348,343.9152507,3385.189959,369.723113,3385.189959,415.274887,3709.838348,436.0827493 +65,5,5,3705.023592,451.5569101,3372.711333,490.7695698,3397.624908,535.897668,3694.700401,543.3891429 +65,10,5,3720.256355,559.5354091,3360.188801,612.2419384,3410.01641,656.0989938,3679.60953,650.3618346 +65,15,5,3735.536859,667.8523311,3347.622129,734.1424707,3422.364691,775.8810692,3664.565516,757.0023785 +70,-15,5,3733.448678,23.63929076,3535.533272,8.297392607,3461.633157,48.1916552,3844.866102,114.2328516 +70,-10,5,3748.56696,129.8317352,3523.324686,128.649902,3474.058707,170.6829984,3829.511174,222.0875232 +70,-5,5,3763.732131,236.3535366,3511.073164,249.425673,3486.440404,292.7420377,3814.204018,329.6066416 +70,0,5,3778.94441,343.2062296,3498.778479,370.6269422,3498.778479,414.3710578,3798.94441,436.7917704 +70,5,5,3794.204018,450.3913584,3486.440404,492.2559623,3511.073164,535.572327,3783.732131,543.6444634 +70,10,5,3809.511174,557.9104768,3474.058707,614.3150016,3523.324686,656.348098,3768.56696,650.1662648 +70,15,5,3824.866102,665.7651484,3461.633157,736.8063448,3535.533272,776.7006074,3753.448678,756.3587092 +75,-15,5,3821.587498,24.27756973,3649.785721,7.470005368,3576.74778,45.50199561,3933.440293,116.3023926 +75,-10,5,3836.778277,130.0256646,3637.720082,128.3984078,3589.028884,168.5899042,3918.012741,223.698742 +75,-5,5,3852.015973,236.1003608,3625.611806,249.7541409,3601.266435,291.2413085,3902.632985,330.762372 +75,0,5,3867.300802,342.5031737,3613.460666,371.5394737,3613.460666,413.4585263,3887.300802,437.4948263 +75,5,5,3882.632985,449.235628,3601.266435,493.7566915,3625.611806,535.2438591,3872.015973,543.8976392 +75,10,5,3898.012741,556.299258,3589.028884,616.4080958,3637.720082,656.5995922,3856.778277,649.9723354 +75,15,5,3913.440293,663.6956074,3576.74778,739.4960044,3649.785721,777.5279946,3841.587498,755.7204303 +80,-15,5,3908.991288,24.91052579,3765.137684,6.634655729,3692.982099,42.78617424,4021.268965,118.3545144 +80,-10,5,3924.25283,130.2179743,3753.218318,128.144489,3705.115356,166.4764872,4005.770552,225.2964204 +80,-5,5,3939.56131,235.8493029,3741.256629,250.085781,3717.205376,289.7260339,3990.319952,331.9084048 +80,0,5,3954.916944,341.8060079,3729.252391,372.4608339,3729.252391,412.5371661,3974.916944,438.1919921 +80,5,5,3970.319952,448.0895952,3717.205376,495.2719661,3741.256629,534.912219,3959.56131,544.1486971 +80,10,5,3985.770552,554.7015796,3705.115356,618.5215128,3753.218318,656.853511,3944.25283,649.7800257 +80,15,5,4001.268965,661.6434856,3692.982099,742.2118258,3765.137684,778.3633443,3928.991288,755.0874742 +85,-15,5,3995.669204,25.53822526,3881.605109,5.791228194,3810.35253,40.04380754,4108.361492,120.3894362 +85,-10,5,4010.99981,130.4086845,3869.835421,127.8881104,3822.33446,164.3424501,4092.793942,226.8807283 +85,-5,5,4026.377371,235.6003364,3858.023738,250.4206396,3834.273485,288.1960016,4077.274219,333.0448615 +85,0,5,4041.802101,341.1146587,3846.169835,373.3911514,3846.169835,411.6068486,4061.802101,438.8833413 +85,5,5,4057.274219,446.9531385,3834.273485,496.8019984,3858.023738,534.5773604,4046.377371,544.3976636 +85,10,5,4072.793942,553.1172717,3822.33446,620.6555499,3869.835421,657.1098896,4030.99981,649.5893155 +85,15,5,4088.361492,659.6085638,3810.35253,744.9541925,3881.605109,779.2067718,4015.669204,754.4597747 +90,-15,5,4081.630251,26.16073335,3999.204255,4.939605024,3928.875813,37.2745044,4194.727089,122.4073732 +90,-10,5,4097.028258,130.597815,3987.587726,127.6292362,3940.702855,162.1874895,4179.092093,228.4518327 +90,-5,5,4112.47323,235.3534352,3975.929547,250.7587637,3952.487338,286.6509949,4163.50493,334.1718617 +90,0,5,4127.965382,340.4290535,3964.229493,374.3305576,3964.229493,410.6674424,4147.965382,439.5689465 +90,5,5,4143.50493,445.8261383,3952.487338,498.3470051,3975.929547,534.2392363,4132.47323,544.6445648 +90,10,5,4159.092093,551.5461673,3940.702855,622.8105105,3987.587726,657.3687638,4117.028258,649.400185 +90,15,5,4174.727089,657.5906268,3928.875813,747.7234956,3999.204255,780.058395,4101.630251,753.8372667 +95,-15,5,4166.883286,26.77811419,4117.951698,4.079666179,4048.569018,34.47786599,4280.37482,124.4085372 +95,-10,5,4182.347064,130.7853854,4106.491892,127.3678296,4060.237525,160.0112962,4264.674033,230.0098982 +95,-5,5,4197.857813,235.1085738,4094.990796,251.1002013,4071.863838,285.0907928,4249.021081,335.2895228 +95,0,5,4213.415747,339.7491209,4083.448186,375.2791863,4083.448186,409.7188137,4233.415747,440.2488791 +95,5,5,4229.021081,444.7084772,4071.863838,499.9072072,4094.990796,533.8977987,4217.857813,544.8894262 +95,10,5,4244.674033,549.9881018,4060.237525,624.9867038,4106.491892,657.6301704,4202.347064,649.2126146 +95,15,5,4260.37482,655.5894628,4048.569018,750.520134,4117.951698,780.9183338,4186.883286,753.2198858 +100,-15,5,4251.437019,27.39043087,4237.86434,3.211289264,4169.449551,31.65348557,4365.313598,126.3931365 +100,-10,5,4266.964972,130.9714149,4226.564902,127.1038533,4180.955793,157.8135549,4349.548641,231.5550863 +100,-5,5,4282.539897,234.8657269,4215.224552,251.4450013,4192.420222,283.5151702,4333.831516,336.3979605 +100,0,5,4298.162007,339.074791,4203.843067,376.237174,4203.843067,408.760826,4318.162007,440.923209 +100,5,5,4313.831516,443.6000395,4192.420222,501.4828298,4215.224552,533.5529987,4302.539897,545.1322731 +100,10,5,4329.548641,548.4429137,4180.955793,627.1844451,4226.564902,657.8941467,4286.964972,649.0265851 +100,15,5,4345.313598,653.6048635,4169.449551,753.3445144,4237.86434,781.7867107,4271.437019,752.6075691 +105,-15,5,4335.300021,27.99774544,4358.959414,2.33434947,4291.535168,28.8009483,4449.55219,128.3613759 +105,-10,5,4350.890585,131.1559223,4347.824077,126.8372693,4302.875326,155.5939439,4433.72465,233.0875561 +105,-5,5,4366.528117,234.6248699,4336.648222,251.7932137,4314.17407,281.9238972,4417.944935,337.4972885 +105,0,5,4382.212829,338.4059947,4325.431627,377.2046598,4325.431627,407.7933402,4402.212829,441.5920053 +105,5,5,4397.944935,442.5007115,4314.17407,503.0741028,4336.648222,533.2047863,4386.528117,545.3731301 +105,10,5,4413.72465,546.9104439,4302.875326,629.4040561,4347.824077,658.1607307,4370.890585,648.8420777 +105,15,5,4429.55219,651.6366241,4291.535168,756.1970517,4358.959414,782.6636505,4355.300021,752.0002546 +-105,-15,10,150.7783478,-34.23169918,60.56340501,7.04278205,-34.79151876,151.1370688,239.2003059,55.71605962 +-105,-10,10,162.07778,91.03179396,44.79108036,110.7225812,-18.77813756,256.4014596,227.691651,183.298957 +-105,-5,10,173.4185978,216.7540802,28.97098182,214.7164233,-2.813407763,361.3460392,216.225303,310.4128484 +-105,0,10,184.801029,342.9376849,13.10289202,319.0257372,13.10289202,465.9722628,204.801029,437.0603151 +-105,5,10,196.225303,469.5851516,-2.813407763,423.6519608,28.97098182,570.2815767,193.4185978,563.2439198 +-105,10,10,207.691651,596.699043,-18.77813756,528.5965404,44.79108036,674.2754188,182.07778,688.966206 +-105,15,10,219.2003059,724.2819404,-34.79151876,633.8609312,60.56340501,777.955218,170.7783478,814.2296992 +-100,-15,10,272.4183323,-33.19315433,144.6190594,6.325126853,49.63897151,149.2527836,361.8383911,58.4530463 +-100,-10,10,283.8831174,91.45252263,128.9092294,110.4320281,65.5899052,254.9574458,350.1623854,185.3951231 +-100,-5,10,295.3896856,216.5524672,113.1516179,214.8555703,81.49217615,360.3396268,338.5290883,311.8728709 +-100,0,10,306.9382657,342.1091673,97.34600672,319.5972001,97.34600672,465.4007999,326.9382657,437.8888327 +-100,5,10,318.5290883,468.1251291,81.49217615,424.6583732,113.1516179,570.1424297,315.3896856,563.4455328 +-100,10,10,330.1623854,594.6028769,65.5899052,530.0405542,128.9092294,674.5659719,303.8831174,688.5454774 +-100,15,10,341.8383911,721.5449537,49.63897151,635.7452164,144.6190594,778.6728731,292.4183323,813.1911543 +-95,-15,10,392.8628477,-32.16481622,229.3708221,5.601528384,134.7751201,147.3527498,483.2579893,61.16283928 +-95,-10,10,404.4891939,91.86910892,213.7250664,110.1390652,150.6620026,253.5013817,471.418514,187.4705005 +-95,-5,10,416.157703,216.3528428,198.0315265,214.9958732,166.5002159,359.3248286,459.6221308,313.31844 +-95,0,10,427.8686048,341.2888368,182.2899832,320.1734173,182.2899832,464.8245827,447.8686048,438.7091632 +-95,5,10,439.6221308,466.67956,166.5002159,425.6731714,198.0315265,570.0021268,436.157703,563.6451572 +-95,10,10,451.418514,592.5274995,150.6620026,531.4966183,213.7250664,674.8589348,424.4891939,688.1288911 +-95,15,10,463.2579893,718.8351607,134.7751201,637.6452502,229.3708221,779.3964716,412.8628477,812.1628162 +-90,-15,10,512.1294312,-31.14653513,314.8273763,4.871912505,220.6258108,145.4367692,603.4771704,63.84584183 +-90,-10,10,523.913634,92.28161371,299.2473076,109.8436622,236.4470046,252.0331157,591.4780164,189.5253968 +-90,-5,10,535.7403625,216.1551778,283.619457,215.1373464,252.2195277,358.3015396,579.5223205,314.7497692 +-90,0,10,547.6098472,340.4765725,267.9436041,320.7544483,267.9436041,464.2435517,567.6098472,439.5214275 +-90,5,10,559.5223205,465.2482308,252.2195277,426.6964604,283.619457,569.8606536,555.7403625,563.8428222 +-90,10,10,571.4780164,590.4726032,236.4470046,532.9648843,299.2473076,675.1543378,543.913634,687.7163863 +-90,15,10,583.4771704,716.1521582,220.6258108,639.5612308,314.8273763,780.1260875,532.1294312,811.1445351 +-85,-15,10,630.2352788,-30.13816424,400.9975501,4.136203842,307.2000773,143.5046401,722.5136486,66.50244931 +-85,-10,10,642.1737187,92.6900967,385.4848151,109.5457886,322.9539102,250.5524939,710.358519,191.5601137 +-85,-5,10,654.1550306,215.9594434,369.9243054,215.2800046,338.659076,357.2696526,698.2471969,316.1670679 +-85,0,10,666.1794456,339.6722561,354.3157998,321.3403538,354.3157998,463.6576462,686.1794456,440.3257439 +-85,5,10,678.2471969,463.8309321,338.659076,427.7283474,369.9243054,569.7179954,674.1550306,564.0385566 +-85,10,10,690.358519,588.4378863,322.9539102,534.4455061,385.4848151,675.4522114,662.1737187,687.3079033 +-85,15,10,702.5136486,713.4955507,307.2000773,641.4933599,400.9975501,780.8617962,650.2352788,810.1361642 +-80,-15,10,747.1972532,-29.13955957,487.89032,3.394325758,394.5071059,141.5561575,840.3847915,69.13304931 +-80,-10,10,759.2863937,93.09461641,472.4465998,109.2454133,410.1918704,249.0593596,828.0773041,193.5749471 +-80,-5,10,771.4187361,215.7656115,456.9551173,215.4238628,425.8279766,356.2290588,815.8139568,317.5705413 +-80,0,10,783.5945126,338.8757714,441.4156507,321.9311953,441.4156507,463.0668047,803.5945126,441.1222286 +-80,5,10,795.8139568,462.4274587,425.8279766,428.7689412,456.9551173,569.5741372,791.4187361,564.2323885 +-80,10,10,808.0773041,586.4230529,410.1918704,535.9386404,472.4465998,675.7525867,779.2863937,686.9033836 +-80,15,10,820.3847915,710.8649507,394.5071059,643.4418425,487.89032,781.6036742,767.1972532,809.1375596 +-75,-15,10,863.0318924,-28.15057993,575.5148134,2.646200323,482.5562394,139.5911129,957.1076282,71.73802189 +-75,-10,10,875.2682771,93.49523024,560.1418245,108.9425046,498.1701914,247.5535535,944.6513174,195.570187 +-75,-5,10,887.5481786,215.5736545,544.7210914,215.5689362,513.7354997,355.1796476,932.2394637,318.9603909 +-75,0,10,899.8718294,338.0870046,529.2523911,322.5270356,529.2523911,462.4709644,919.8718294,441.9109954 +-75,5,10,912.2394637,461.0376091,513.7354997,429.8183524,544.7210914,569.4290638,907.5481786,564.4243455 +-75,10,10,924.6513174,584.427813,498.1701914,537.4444465,560.1418245,676.0554954,895.2682771,686.5027698 +-75,15,10,937.1076282,708.2599781,482.5562394,645.4068871,575.5148134,782.3517997,883.0318924,808.1485799 +-70,-15,10,977.7554169,-27.17108683,663.880312,1.891748291,571.3569799,137.6092943,1072.698858,74.31773971 +-70,-10,10,990.1356682,93.8919945,648.5798074,108.6370304,586.8983383,246.0349137,1060.097177,197.5461178 +-70,-5,10,1002.559736,215.3835453,633.231582,215.7152403,602.3910732,354.1213064,1047.540255,320.336814 +-70,0,10,1015.027853,337.3058441,617.8354123,323.1279382,617.8354123,461.8700618,1035.027853,442.6921559 +-70,5,10,1027.540255,459.661186,602.3910732,430.8766936,633.231582,569.2827597,1022.559736,564.6144547 +-70,10,10,1040.097177,582.4518822,586.8983383,538.9630863,648.5798074,676.3609696,1010.135668,686.1060055 +-70,15,10,1052.698858,705.6802603,571.3569799,647.3887057,663.880312,783.1062517,997.7554169,807.1690868 +-65,-15,10,1091.383737,-26.20094443,752.9962554,1.130889068,660.9189924,135.6104859,1187.174857,76.87256828 +-65,-10,10,1103.904554,94.2849644,737.770025,108.3289578,676.3859381,244.5032753,1174.431181,199.5030185 +-65,-5,10,1116.469473,215.1952573,722.4961031,215.8627908,691.8042863,353.0539207,1161.73255,321.7000042 +-65,0,10,1129.078726,336.5321804,707.1742655,323.733968,707.1742655,461.264032,1149.078726,443.4658196 +-65,5,10,1141.73255,458.2979958,691.8042863,431.9440793,722.4961031,569.1352092,1136.469473,564.8027427 +-65,10,10,1154.431181,580.4949815,676.3859381,540.4947247,737.770025,676.6690422,1123.904554,685.7130356 +-65,15,10,1167.174857,703.1254317,660.9189924,649.3875141,752.9962554,783.8671109,1111.383737,806.1989444 +-60,-15,10,1203.932462,-25.24001946,842.8722441,0.363540687,751.2521086,133.5944683,1300.551687,79.40286606 +-60,-10,10,1216.590618,94.67419412,827.7221158,108.0182537,766.6427833,242.9584709,1287.669314,201.4411626 +-60,-5,10,1229.293146,215.0087646,812.5243315,216.0116036,781.9848926,351.9773742,1274.832258,323.0501514 +-60,0,10,1242.040281,335.7659061,797.278666,324.3451908,797.278666,460.6528092,1262.040281,444.2320939 +-60,5,10,1254.832258,456.9478486,781.9848926,433.0206258,812.5243315,568.9863964,1249.293146,564.9892354 +-60,10,10,1267.669314,578.5568374,766.6427833,542.0395291,827.7221158,676.9797463,1236.590618,685.3238059 +-60,15,10,1280.551687,700.5951339,751.2521086,651.4035317,842.8722441,784.6344593,1223.932462,805.2380195 +-55,-15,10,1315.416905,-24.28818117,933.5180431,-0.410380225,842.3663299,131.5610183,1412.845105,81.90898467 +-55,-10,10,1328.209245,95.0597368,918.4458839,107.7048841,857.6788355,241.4003297,1399.827256,203.3608186 +-55,-5,10,1341.046215,214.8240415,903.3261102,216.1616951,872.9428141,350.8915483,1386.854984,324.387442 +-55,0,10,1353.92805,335.0069158,888.1584961,324.9616738,888.1584961,460.0363262,1373.92805,444.9910842 +-55,5,10,1366.854984,455.610558,872.9428141,434.1064517,903.3261102,568.8363049,1361.046215,565.1739585 +-55,10,10,1379.827256,576.6371814,857.6788355,543.5976703,918.4458839,677.2931159,1348.209245,684.9382632 +-55,15,10,1392.845105,698.0890153,842.3663299,653.4369817,933.5180431,785.4083802,1335.416905,804.2861812 +-50,-15,10,1425.852091,-23.3453013,1024.943585,-1.190958473,934.2718315,129.5099089,1524.070565,84.39126903 +-50,-10,10,1438.775531,95.44164458,1009.951302,107.3888146,949.504229,239.8286785,1510.920392,205.2622497 +-50,-5,10,1451.743847,214.641063,994.911452,216.3130817,964.6881445,349.7963226,1497.81604,325.7120588 +-50,0,10,1464.757271,334.2551062,979.8238089,325.5834852,979.8238089,459.4145148,1484.757271,445.7428938 +-50,5,10,1477.81604,454.2859412,964.6881445,435.2016774,994.911452,568.6849183,1471.743847,565.356937 +-50,10,10,1490.920392,574.7357503,949.504229,545.1693215,1009.951302,677.6091854,1458.775531,684.5563554 +-50,15,10,1504.070565,695.606731,934.2718315,655.4880911,1024.943585,786.1889585,1445.852091,803.3433013 +-45,-15,10,1535.252763,-22.41125395,1117.158975,-1.978280329,1026.978966,127.440909,1634.243231,86.85005757 +-45,-10,10,1548.304289,95.81996863,1102.248515,107.0700101,1042.129275,238.2433408,1620.963812,207.1457143 +-45,-5,10,1561.400922,214.4598046,1087.290543,216.4657804,1057.231153,348.6915745,1607.730447,327.024181 +-45,0,10,1574.542896,333.5103758,1072.284832,326.2106942,1072.284832,458.7873058,1594.542896,446.4876242 +-45,5,10,1587.730447,452.973819,1057.231153,436.3064255,1087.290543,568.5322196,1581.400922,565.5381954 +-45,10,10,1600.963812,572.8522857,1042.129275,546.7546592,1102.248515,677.9279899,1568.304289,684.1780314 +-45,15,10,1614.243231,693.1479424,1026.978966,657.557091,1117.158975,786.9762803,1555.252763,802.409254 +-40,-15,10,1643.63339,-21.48591561,1210.174493,-2.772433562,1120.498268,125.3537834,1743.377979,89.28568228 +-40,-10,10,1656.810055,96.19475915,1195.347846,106.7484351,1135.564464,236.6441369,1729.972325,209.0114657 +-40,-5,10,1670.032045,214.280242,1180.473748,216.6198083,1150.582288,347.5771792,1716.612943,328.3239845 +-40,0,10,1683.299596,332.7726251,1165.551972,326.8433715,1165.551972,458.1546285,1703.299596,447.2253749 +-40,5,10,1696.612943,451.6740155,1150.582288,437.4208208,1180.473748,568.3781917,1690.032045,565.717758 +-40,10,10,1709.972325,570.9865343,1135.564464,548.3538631,1195.347846,678.2495649,1676.810055,683.8032408 +-40,15,10,1723.377979,690.7123177,1120.498268,659.6442166,1210.174493,787.7704336,1663.63339,801.4839156 +-35,-15,10,1751.008173,-20.56916502,1304.000598,-3.57350747,1214.840457,123.248293,1851.489408,91.69846899 +-35,-10,10,1764.307093,96.56606542,1289.259795,106.4240531,1229.820472,235.0308841,1837.960461,210.8597527 +-35,-5,10,1777.651546,214.1023515,1274.471611,216.7751828,1244.752181,346.4530099,1824.477994,329.6116422 +-35,0,10,1791.041768,332.0417565,1259.635816,327.4815889,1259.635816,457.5164111,1811.041768,447.9562435 +-35,5,10,1804.477994,450.3863578,1244.752181,438.5449901,1274.471611,568.2228172,1797.651546,565.8956485 +-35,10,10,1817.960461,569.1382473,1229.820472,549.9671159,1289.259795,678.5739469,1784.307093,683.4319346 +-35,15,10,1831.489408,688.299531,1214.840457,661.749707,1304.000598,788.5715075,1771.008173,800.567165 +-30,-15,10,1857.391048,-19.6608832,1398.647931,-4.381592915,1310.016442,121.1241942,1958.591845,94.0887374 +-30,-10,10,1870.809405,96.9339358,1383.99505,106.0968274,1324.908163,233.4033966,1944.942481,212.6908194 +-30,-5,10,1884.273492,213.92611,1369.294863,216.9319216,1339.751651,345.3189373,1931.339793,330.8873233 +-30,0,10,1897.783543,331.317674,1354.54714,328.1254196,1354.54714,456.8725804,1917.783543,448.680326 +-30,5,10,1911.339793,449.1106767,1339.751651,439.6790627,1369.294863,568.0660784,1904.273492,566.07189 +-30,10,10,1924.942481,567.3071806,1324.908163,551.5946034,1383.99505,678.9011726,1890.809405,683.0640642 +-30,15,10,1938.591845,685.9092626,1310.016442,663.8738058,1398.647931,789.3795929,1877.391048,799.6588832 +-25,-15,10,1962.795697,-18.76095332,1494.127324,-5.196782355,1406.037325,118.9812394,2064.699348,96.45680129 +-25,-10,10,1976.330735,97.29841777,1479.564484,105.7667204,1420.838593,231.761485,2050.93238,214.5049053 +-25,-5,10,1989.911687,213.7514945,1464.954423,217.0900428,1435.591709,344.1748301,2037.212274,332.1511943 +-25,0,10,2003.538788,330.6002837,1450.296908,328.7749378,1450.296908,456.2230622,2023.538788,449.3977163 +-25,5,10,2017.212274,447.8468057,1435.591709,440.8231699,1464.954423,567.9079572,2009.911687,566.2465055 +-25,10,10,2030.93238,565.4930947,1420.838593,553.236515,1479.564484,679.2312796,1996.330735,682.6995822 +-25,15,10,2044.699348,683.5411987,1406.037325,666.0167606,1494.127324,790.1947824,1982.795697,798.7589533 +-20,-15,10,2067.235551,-17.86926073,1590.449795,-6.019169884,1502.914407,116.8191763,2169.825716,98.80296863 +-20,-10,10,2080.884574,97.65955791,1575.979165,105.4336938,1517.623016,230.1049568,2155.943896,216.3022456 +-20,-5,10,2094.579685,213.5784828,1561.461403,217.2495647,1532.283562,343.0205545,2142.10911,333.4034184 +-20,0,10,2108.321118,329.8894931,1546.89628,329.4302193,1546.89628,455.5677807,2128.321118,450.1085069 +-20,5,10,2122.10911,446.5945816,1532.283562,441.9774455,1561.461403,567.7484353,2114.579685,566.4195172 +-20,10,10,2135.943896,563.6957544,1517.623016,554.8930432,1575.979165,679.5643062,2100.884574,682.3384421 +-20,15,10,2149.825716,681.1950314,1502.914407,668.1788237,1590.449795,791.0171699,2087.235551,797.8672607 +-15,-15,10,2170.723797,-16.98569284,1687.626561,-6.848851261,1600.659192,114.6377481,2273.984497,101.1275417 +-15,-10,10,2184.484167,98.01740199,1673.250354,105.0977087,1615.272887,228.4336159,2259.990514,218.083071 +-15,-5,10,2198.290789,213.4070527,1658.827116,217.4105061,1629.838615,341.8559742,2246.043727,334.6441558 +-15,0,10,2212.143898,329.1852117,1644.356614,330.0913411,1644.356614,454.9066589,2232.143898,450.8127883 +-15,5,10,2226.043727,445.3538442,1629.838615,443.1420258,1658.827116,567.5874939,2218.290789,566.5909473 +-15,10,10,2239.990514,561.914929,1615.272887,556.5643841,1673.250354,679.9002913,2204.484167,681.980598 +-15,15,10,2253.984497,678.8704583,1600.659192,670.3602519,1687.626561,791.8468513,2190.723797,796.9836928 +-10,-15,10,2273.27338,-16.11013912,1785.669038,-7.685923955,1699.283389,112.4366936,2377.188987,103.4308174 +-10,-10,10,2287.142519,98.37199492,1771.389517,104.7587255,1713.799865,226.7472627,2363.085471,219.8476081 +-10,-5,10,2301.058063,213.2371828,1757.063073,217.5728859,1728.26848,340.6809507,2349.029304,335.8735639 +-10,0,10,2315.020246,328.4873503,1742.689471,330.7583817,1742.689471,454.2396183,2335.020246,451.5106497 +-10,5,10,2329.029304,444.1244361,1728.26848,444.3170493,1757.063073,567.4251141,2321.058063,566.7608172 +-10,10,10,2343.085471,560.1503919,1713.799865,558.2507373,1771.389517,680.2392745,2307.142519,681.6260051 +-10,15,10,2357.188987,676.5671826,1699.283389,672.5613064,1785.669038,792.683924,2293.27338,796.1081391 +-5,-15,10,2374.897015,-15.242491,1884.588846,-8.530487176,1798.79892,110.2157466,2479.452241,105.7130869 +-5,-10,10,2388.872399,98.72338081,1870.408324,104.4167039,1813.215822,225.0456941,2465.241765,221.5960793 +-5,-5,10,2402.894332,213.0688518,1856.180994,217.7367236,1827.584977,339.4953429,2451.07878,337.0917971 +-5,0,10,2416.963047,327.7958217,1841.906622,331.4314209,1841.906622,453.5665791,2436.963047,452.2021783 +-5,5,10,2431.07878,442.9062029,1827.584977,445.5026571,1856.180994,567.2612764,2422.894332,566.9291482 +-5,10,10,2445.241765,558.4019207,1813.215822,559.9523059,1870.408324,680.5812961,2408.872399,681.2746192 +-5,15,10,2459.452241,674.2849131,1798.79892,674.7822534,1884.588846,793.5284872,2394.897015,795.240491 +0,-15,10,2475.607186,-14.38264192,1984.397814,-9.382641919,1899.217923,107.9746363,2580.787077,107.9746363 +0,-10,10,2489.686347,99.07160297,1970.318653,104.071603,1913.532843,223.3287032,2566.472157,223.3287032 +0,-5,10,2503.81219,212.9020389,1956.19281,217.9020389,1927.800139,338.2990071,2552.204861,338.2990071 +0,0,10,2517.98495,327.11054,1942.02005,332.11054,1942.02005,452.88746,2537.98495,452.88746 +0,5,10,2532.204861,441.6989929,1927.800139,446.6989929,1956.19281,567.0959611,2523.81219,567.0959611 +0,10,10,2546.472157,556.6692968,1913.532843,561.6692968,1970.318653,680.926397,2509.686347,680.926397 +0,15,10,2560.787077,672.0233637,1899.217923,677.0233637,1984.397814,794.3806419,2495.607186,794.3806419 +5,-15,10,2575.416154,-13.53048718,2085.107985,-10.242491,2000.552759,105.7130869,2681.20608,110.2157466 +5,-10,10,2589.596676,99.41670394,2071.132601,103.7233808,2014.763235,221.5960793,2666.789178,225.0456941 +5,-5,10,2603.824006,212.7367236,2057.110668,218.0688518,2028.92622,337.0917971,2652.420023,339.4953429 +5,0,10,2618.098378,326.4314209,2043.041953,332.7958217,2043.041953,452.2021783,2638.098378,453.5665791 +5,5,10,2632.420023,440.5026571,2028.92622,447.9062029,2057.110668,566.9291482,2623.824006,567.2612764 +5,10,10,2646.789178,554.9523059,2014.763235,563.4019207,2071.132601,681.2746192,2609.596676,680.5812961 +5,15,10,2661.20608,669.7822534,2000.552759,679.2849131,2085.107985,795.240491,2595.416154,793.5284872 +10,-15,10,2674.335962,-12.68592395,2186.73162,-11.11013912,2102.816013,103.4308174,2780.721611,112.4366936 +10,-10,10,2688.615483,99.75872549,2172.862481,103.3719949,2116.919529,219.8476081,2766.205135,226.7472627 +10,-5,10,2702.941927,212.5728859,2158.946937,218.2371828,2130.975696,335.8735639,2751.73652,340.6809507 +10,0,10,2717.315529,325.7583817,2144.984754,333.4873503,2144.984754,451.5106497,2737.315529,454.2396183 +10,5,10,2731.73652,439.3170493,2130.975696,449.1244361,2158.946937,566.7608172,2722.941927,567.4251141 +10,10,10,2746.205135,553.2507373,2116.919529,565.1503919,2172.862481,681.6260051,2708.615483,680.2392745 +10,15,10,2760.721611,667.5613064,2102.816013,681.5671826,2186.73162,796.1081391,2694.335962,792.683924 +15,-15,10,2772.378439,-11.84885126,2289.281203,-11.98569284,2206.020503,101.1275417,2879.345808,114.6377481 +15,-10,10,2786.754646,100.0977087,2275.520833,103.017402,2220.014486,218.083071,2864.732113,228.4336159 +15,-5,10,2801.177884,212.4105061,2261.714211,218.4070527,2233.961273,334.6441558,2850.166385,341.8559742 +15,0,10,2815.648386,325.0913411,2247.861102,334.1852117,2247.861102,450.8127883,2835.648386,454.9066589 +15,5,10,2830.166385,438.1420258,2233.961273,450.3538442,2261.714211,566.5909473,2821.177884,567.5874939 +15,10,10,2844.732113,551.5643841,2220.014486,566.914929,2275.520833,681.980598,2806.754646,679.9002913 +15,15,10,2859.345808,665.3602519,2206.020503,683.8704583,2289.281203,796.9836928,2792.378439,791.8468513 +20,-15,10,2869.555205,-11.01916988,2392.769449,-12.86926073,2310.179284,98.80296863,2977.090593,116.8191763 +20,-10,10,2884.025835,100.4336938,2379.120426,102.6595579,2324.061104,216.3022456,2962.381984,230.1049568 +20,-5,10,2898.543597,212.2495647,2365.425315,218.5784828,2337.89589,333.4034184,2947.721438,343.0205545 +20,0,10,2913.10872,324.4302193,2351.683882,334.8894931,2351.683882,450.1085069,2933.10872,455.5677807 +20,5,10,2927.721438,436.9774455,2337.89589,451.5945816,2365.425315,566.4195172,2918.543597,567.7484353 +20,10,10,2942.381984,549.8930432,2324.061104,568.6957544,2379.120426,682.3384421,2904.025835,679.5643062 +20,15,10,2957.090593,663.1788237,2310.179284,686.1950314,2392.769449,797.8672607,2889.555205,791.0171699 +25,-15,10,2965.877676,-10.19678236,2497.209303,-13.76095332,2415.305652,96.45680129,3073.967675,118.9812394 +25,-10,10,2980.440516,100.7667204,2483.674265,102.2984178,2429.07262,214.5049053,3059.166407,231.761485 +25,-5,10,2995.050577,212.0900428,2470.093313,218.7514945,2442.792726,332.1511943,3044.413291,344.1748301 +25,0,10,3009.708092,323.7749378,2456.466212,335.6002837,2456.466212,449.3977163,3029.708092,456.2230622 +25,5,10,3024.413291,435.8231699,2442.792726,452.8468057,2470.093313,566.2465055,3015.050577,567.9079572 +25,10,10,3039.166407,548.236515,2429.07262,570.4930947,2483.674265,682.6995822,3000.440516,679.2312796 +25,15,10,3053.967675,661.0167606,2415.305652,688.5411987,2497.209303,798.7589533,2985.877676,790.1947824 +30,-15,10,3061.357069,-9.381592915,2602.613952,-14.6608832,2521.413155,94.0887374,3169.988558,121.1241942 +30,-10,10,3076.00995,101.0968274,2589.195595,101.9339358,2535.062519,212.6908194,3155.096837,233.4033966 +30,-5,10,3090.710137,211.9319216,2575.731508,218.92611,2548.665207,330.8873233,3140.253349,345.3189373 +30,0,10,3105.45786,323.1254196,2562.221457,336.317674,2562.221457,448.680326,3125.45786,456.8725804 +30,5,10,3120.253349,434.6790627,2548.665207,454.1106767,2575.731508,566.07189,3110.710137,568.0660784 +30,10,10,3135.096837,546.5946034,2535.062519,572.3071806,2589.195595,683.0640642,3096.00995,678.9011726 +30,15,10,3149.988558,658.8738058,2521.413155,690.9092626,2602.613952,799.6588832,3081.357069,789.3795929 +35,-15,10,3156.004402,-8.57350747,2708.996827,-15.56916502,2628.515592,91.69846899,3265.164543,123.248293 +35,-10,10,3170.745205,101.4240531,2695.697907,101.5660654,2642.044539,210.8597527,3250.184528,235.0308841 +35,-5,10,3185.533389,211.7751828,2682.353454,219.1023515,2655.527006,329.6116422,3235.252819,346.4530099 +35,0,10,3200.369184,322.4815889,2668.963232,337.0417565,2668.963232,447.9562435,3220.369184,457.5164111 +35,5,10,3215.252819,433.5449901,2655.527006,455.3863578,2682.353454,565.8956485,3205.533389,568.2228172 +35,10,10,3230.184528,544.9671159,2642.044539,574.1382473,2695.697907,683.4319346,3190.745205,678.5739469 +35,15,10,3245.164543,656.749707,2628.515592,693.299531,2708.996827,800.567165,3176.004402,788.5715075 +40,-15,10,3249.830507,-7.772433562,2816.37161,-16.48591561,2736.627021,89.28568228,3359.506732,125.3537834 +40,-10,10,3264.657154,101.7484351,2803.194945,101.1947592,2750.032675,209.0114657,3344.440536,236.6441369 +40,-5,10,3279.531252,211.6198083,2789.972955,219.280242,2763.392057,328.3239845,3329.422712,347.5771792 +40,0,10,3294.453028,321.8433715,2776.705404,337.7726251,2776.705404,447.2253749,3314.453028,458.1546285 +40,5,10,3309.422712,432.4208208,2763.392057,456.6740155,2789.972955,565.717758,3299.531252,568.3781917 +40,10,10,3324.440536,543.3538631,2750.032675,575.9865343,2803.194945,683.8032408,3284.657154,678.2495649 +40,15,10,3339.506732,654.6442166,2736.627021,695.7123177,2816.37161,801.4839156,3269.830507,787.7704336 +45,-15,10,3342.846025,-6.978280329,2924.752237,-17.41125395,2845.761769,86.85005757,3453.026034,127.440909 +45,-10,10,3357.756485,102.0700101,2911.700711,100.8199686,2859.041188,207.1457143,3437.875725,238.2433408 +45,-5,10,3372.714457,211.4657804,2898.604078,219.4598046,2872.274553,327.024181,3422.773847,348.6915745 +45,0,10,3387.720168,321.2106942,2885.462104,338.5103758,2885.462104,446.4876242,3407.720168,458.7873058 +45,5,10,3402.773847,431.3064255,2872.274553,457.973819,2898.604078,565.5381954,3392.714457,568.5322196 +45,10,10,3417.875725,541.7546592,2859.041188,577.8522857,2911.700711,684.1780314,3377.756485,677.9279899 +45,15,10,3433.026034,652.557091,2845.761769,698.1479424,2924.752237,802.409254,3362.846025,786.9762803 +50,-15,10,3435.061415,-6.190958473,3034.152909,-18.3453013,2955.934435,84.39126903,3545.733168,129.5099089 +50,-10,10,3450.053698,102.3888146,3021.229469,100.4416446,2969.084608,205.2622497,3530.500771,239.8286785 +50,-5,10,3465.093548,211.3130817,3008.261153,219.641063,2982.18896,325.7120588,3515.316856,349.7963226 +50,0,10,3480.181191,320.5834852,2995.247729,339.2551062,2995.247729,445.7428938,3500.181191,459.4145148 +50,5,10,3495.316856,430.2016774,2982.18896,459.2859412,3008.261153,565.356937,3485.093548,568.6849183 +50,10,10,3510.500771,540.1693215,2969.084608,579.7357503,3021.229469,684.5563554,3470.053698,677.6091854 +50,15,10,3525.733168,650.4880911,2955.934435,700.606731,3034.152909,803.3433013,3455.061415,786.1889585 +55,-15,10,3526.486957,-5.410380225,3144.588095,-19.28818117,3067.159895,81.90898467,3637.63867,131.5610183 +55,-10,10,3541.559116,102.7048841,3131.795755,100.0597368,3080.177744,203.3608186,3622.326164,241.4003297 +55,-5,10,3556.67889,211.1616951,3118.958785,219.8240415,3093.150016,324.387442,3607.062186,350.8915483 +55,0,10,3571.846504,319.9616738,3106.07695,340.0069158,3106.07695,444.9910842,3591.846504,460.0363262 +55,5,10,3587.062186,429.1064517,3093.150016,460.610558,3118.958785,565.1739585,3576.67889,568.8363049 +55,10,10,3602.326164,538.5976703,3080.177744,581.6371814,3131.795755,684.9382632,3561.559116,677.2931159 +55,15,10,3617.63867,648.4369817,3067.159895,703.0890153,3144.588095,804.2861812,3546.486957,785.4083802 +60,-15,10,3617.132756,-4.636459313,3256.072538,-20.24001946,3179.453313,79.40286606,3728.752891,133.5944683 +60,-10,10,3632.282884,103.0182537,3243.414382,99.67419412,3192.335686,201.4411626,3713.362217,242.9584709 +60,-5,10,3647.480668,211.0116036,3230.711854,220.0087646,3205.172742,323.0501514,3698.020107,351.9773742 +60,0,10,3662.726334,319.3451908,3217.964719,340.7659061,3217.964719,444.2320939,3682.726334,460.6528092 +60,5,10,3678.020107,428.0206258,3205.172742,461.9478486,3230.711854,564.9892354,3667.480668,568.9863964 +60,10,10,3693.362217,537.0395291,3192.335686,583.5568374,3243.414382,685.3238059,3652.282884,676.9797463 +60,15,10,3708.752891,646.4035317,3179.453313,705.5951339,3256.072538,805.2380195,3637.132756,784.6344593 +65,-15,10,3707.008745,-3.869110932,3368.621263,-21.20094443,3292.830143,76.87256828,3819.086008,135.6104859 +65,-10,10,3722.234975,103.3289578,3356.100446,99.2849644,3305.573819,199.5030185,3803.619062,244.5032753 +65,-5,10,3737.508897,210.8627908,3343.535527,220.1952573,3318.27245,321.7000042,3788.200714,353.0539207 +65,0,10,3752.830734,318.733968,3330.926274,341.5321804,3330.926274,443.4658196,3772.830734,461.264032 +65,5,10,3768.200714,426.9440793,3318.27245,463.2979958,3343.535527,564.8027427,3757.508897,569.1352092 +65,10,10,3783.619062,535.4947247,3305.573819,585.4949815,3356.100446,685.7130356,3742.234975,676.6690422 +65,15,10,3799.086008,644.3875141,3292.830143,708.1254317,3368.621263,806.1989444,3727.008745,783.8671109 +70,-15,10,3796.124688,-3.108251709,3482.249583,-22.17108683,3407.306142,74.31773971,3908.64802,137.6092943 +70,-10,10,3811.425193,103.6370304,3469.869332,98.8919945,3419.907823,197.5461178,3893.106662,246.0349137 +70,-5,10,3826.773418,210.7152403,3457.445264,220.3835453,3432.464745,320.336814,3877.613927,354.1213064 +70,0,10,3842.169588,318.1279382,3444.977147,342.3058441,3444.977147,442.6921559,3862.169588,461.8700618 +70,5,10,3857.613927,425.8766936,3432.464745,464.661186,3457.445264,564.6144547,3846.773418,569.2827597 +70,10,10,3873.106662,533.9630863,3419.907823,587.4518822,3469.869332,686.1060055,3831.425193,676.3609696 +70,15,10,3888.64802,642.3887057,3407.306142,710.6802603,3482.249583,807.1690868,3816.124688,783.1062517 +75,-15,10,3884.490187,-2.353799677,3596.973108,-23.15057993,3522.897372,71.73802189,3997.448761,139.5911129 +75,-10,10,3899.863175,103.9425046,3584.736723,98.49523024,3535.353683,195.570187,3981.834809,247.5535535 +75,-5,10,3915.283909,210.5689362,3572.456821,220.5736545,3547.765536,318.9603909,3966.2695,355.1796476 +75,0,10,3930.752609,317.5270356,3560.133171,343.0870046,3560.133171,441.9109954,3950.752609,462.4709644 +75,5,10,3946.2695,424.8183524,3547.765536,466.0376091,3572.456821,564.4243455,3935.283909,569.4290638 +75,10,10,3961.834809,532.4444465,3535.353683,589.427813,3584.736723,686.5027698,3919.863175,676.0554954 +75,15,10,3977.448761,640.4068871,3522.897372,713.2599781,3596.973108,808.1485799,3904.490187,782.3517997 +80,-15,10,3972.11468,-1.605674242,3712.807747,-24.13955957,3639.620209,69.13304931,4085.497894,141.5561575 +80,-10,10,3987.5584,104.2454133,3700.718606,98.09461641,3651.927696,193.5749471,4069.81313,249.0593596 +80,-5,10,4003.049883,210.4238628,3688.586264,220.7656115,3664.191043,317.5705413,4054.177023,356.2290588 +80,0,10,4018.589349,316.9311953,3676.410487,343.8757714,3676.410487,441.1222286,4038.589349,463.0668047 +80,5,10,4034.177023,423.7689412,3664.191043,467.4274587,3688.586264,564.2323885,4023.049883,569.5741372 +80,10,10,4049.81313,530.9386404,3651.927696,591.4230529,3700.718606,686.9033836,4007.5584,675.7525867 +80,15,10,4065.497894,638.4418425,3639.620209,715.8649507,3712.807747,809.1375596,3992.11468,781.6036742 +85,-15,10,4059.00745,-0.863796158,3829.769721,-25.13816424,3757.491351,66.50244931,4172.804923,143.5046401 +85,-10,10,4074.520185,104.5457886,3817.831281,97.6900967,3769.646481,191.5601137,4157.05109,250.5524939 +85,-5,10,4090.080695,210.2800046,3805.849969,220.9594434,3781.757803,316.1670679,4141.345924,357.2696526 +85,0,10,4105.6892,316.3403538,3793.825554,344.6722561,3793.825554,440.3257439,4125.6892,463.6576462 +85,5,10,4121.345924,422.7283474,3781.757803,468.8309321,3805.849969,564.0385566,4110.080695,569.7179954 +85,10,10,4137.05109,529.4455061,3769.646481,593.4378863,3817.831281,687.3079033,4094.520185,675.4522114 +85,15,10,4152.804923,636.4933599,3757.491351,718.4955507,3829.769721,810.1361642,4079.00745,780.8617962 +90,-15,10,4145.177624,-0.128087495,3947.875569,-26.14653513,3876.52783,63.84584183,4259.379189,145.4367692 +90,-10,10,4160.757692,104.8436622,3936.091366,97.28161371,3888.526984,189.5253968,4243.557995,252.0331157 +90,-5,10,4176.385543,210.1373464,3924.264638,221.1551778,3900.482679,314.7497692,4227.785472,358.3015396 +90,0,10,4192.061396,315.7544483,3912.395153,345.4765725,3912.395153,439.5214275,4212.061396,464.2435517 +90,5,10,4207.785472,421.6964604,3900.482679,470.2482308,3924.264638,563.8428222,4196.385543,569.8606536 +90,10,10,4223.557995,527.9648843,3888.526984,595.4726032,3936.091366,687.7163863,4180.757692,675.1543378 +90,15,10,4239.379189,634.5612308,3876.52783,721.1521582,3947.875569,811.1445351,4165.177624,780.1260875 +95,-15,10,4230.634178,0.601528384,4067.142152,-27.16481622,3996.747011,61.16283928,4345.22988,147.3527498 +95,-10,10,4246.279934,105.1390652,4055.515806,96.86910892,4008.586486,187.4705005,4329.342997,253.5013817 +95,-5,10,4261.973474,209.9958732,4043.847297,221.3528428,4020.382869,313.31844,4313.504784,359.3248286 +95,0,10,4277.715017,315.1734173,4032.136395,346.2888368,4032.136395,438.7091632,4297.715017,464.8245827 +95,5,10,4293.504784,420.6731714,4020.382869,471.67956,4043.847297,563.6451572,4281.973474,570.0021268 +95,10,10,4309.342997,526.4966183,4008.586486,597.5274995,4055.515806,688.1288911,4266.279934,674.8589348 +95,15,10,4325.22988,632.6452502,3996.747011,723.8351607,4067.142152,812.1628162,4250.634178,779.3964716 +100,-15,10,4315.385941,1.325126853,4187.586668,-28.19315433,4118.166609,58.4530463,4430.366028,149.2527836 +100,-10,10,4331.095771,105.4320281,4176.121883,96.45252263,4129.842615,185.3951231,4414.415095,254.9574458 +100,-5,10,4346.853382,209.8555703,4164.615314,221.5524672,4141.475912,311.8728709,4398.512824,360.3396268 +100,0,10,4362.658993,314.5972001,4153.066734,347.1091673,4153.066734,437.8888327,4382.658993,465.4007999 +100,5,10,4378.512824,419.6583732,4141.475912,473.1251291,4164.615314,563.4455328,4366.853382,570.1424297 +100,10,10,4394.415095,525.0405542,4129.842615,599.6028769,4176.121883,688.5454774,4351.095771,674.5659719 +100,15,10,4410.366028,630.7452164,4118.166609,726.5449537,4187.586668,813.1911543,4335.385941,778.6728731 +105,-15,10,4399.441595,2.04278205,4309.226652,-29.23169918,4240.804694,55.71605962,4514.796519,151.1370688 +105,-10,10,4415.21392,105.7225812,4297.92722,96.03179396,4252.313349,183.298957,4498.783138,256.4014596 +105,-5,10,4431.034018,209.7164233,4286.586402,221.7540802,4263.779697,310.4128484,4482.818408,361.3460392 +105,0,10,4446.902108,314.0257372,4275.203971,347.9376849,4275.203971,437.0603151,4466.902108,465.9722628 +105,5,10,4462.818408,418.6519608,4263.779697,474.5851516,4286.586402,563.2439198,4451.034018,570.2815767 +105,10,10,4478.783138,523.5965404,4252.313349,601.699043,4297.92722,688.966206,4435.21392,674.2754188 +105,15,10,4494.796519,628.8609312,4240.804694,729.2819404,4309.226652,814.2296992,4419.441595,777.955218 diff --git a/openpiv/data/test7/D_cal_cam0_0001.tif b/openpiv/data/test7/D_cal_cam0_0001.tif new file mode 100644 index 00000000..b63d1e41 Binary files /dev/null and b/openpiv/data/test7/D_cal_cam0_0001.tif differ diff --git a/openpiv/data/test7/D_cam0_0000.tif b/openpiv/data/test7/D_cam0_0000.tif new file mode 100644 index 00000000..47efe466 Binary files /dev/null and b/openpiv/data/test7/D_cam0_0000.tif differ diff --git a/openpiv/data/test7/D_cam0_0001.tif b/openpiv/data/test7/D_cam0_0001.tif new file mode 100644 index 00000000..798dd48e Binary files /dev/null and b/openpiv/data/test7/D_cam0_0001.tif differ diff --git a/openpiv/data/test7/D_cam1_0000.tif b/openpiv/data/test7/D_cam1_0000.tif new file mode 100644 index 00000000..b14a07ae Binary files /dev/null and b/openpiv/data/test7/D_cam1_0000.tif differ diff --git a/openpiv/data/test7/D_cam1_0001.tif b/openpiv/data/test7/D_cam1_0001.tif new file mode 100644 index 00000000..055f3008 Binary files /dev/null and b/openpiv/data/test7/D_cam1_0001.tif differ diff --git a/openpiv/data/test7/D_cam2_0000.tif b/openpiv/data/test7/D_cam2_0000.tif new file mode 100644 index 00000000..9dc5a0d7 Binary files /dev/null and b/openpiv/data/test7/D_cam2_0000.tif differ diff --git a/openpiv/data/test7/D_cam2_0001.tif b/openpiv/data/test7/D_cam2_0001.tif new file mode 100644 index 00000000..83cfc50c Binary files /dev/null and b/openpiv/data/test7/D_cam2_0001.tif differ diff --git a/openpiv/data/test7/D_cam3_0000.tif b/openpiv/data/test7/D_cam3_0000.tif new file mode 100644 index 00000000..6e29183d Binary files /dev/null and b/openpiv/data/test7/D_cam3_0000.tif differ diff --git a/openpiv/data/test7/D_cam3_0001.tif b/openpiv/data/test7/D_cam3_0001.tif new file mode 100644 index 00000000..86257c2b Binary files /dev/null and b/openpiv/data/test7/D_cam3_0001.tif differ diff --git a/openpiv/data/test7/READEME.md b/openpiv/data/test7/READEME.md new file mode 100644 index 00000000..2a907fae --- /dev/null +++ b/openpiv/data/test7/READEME.md @@ -0,0 +1 @@ +The following dataset is taken from PIV Challenge for use in OpenPIV. PIV Challenge is an initiative to provide PIV datasets in order to assess the current state-of-the-art PIV algorithms. The test case selected for this dataset is the 4th International PIV Challenge Case D since it coverers multiple-camera imaging systems and tomographic PIV. The complete dataset along with further information can be found at https://www.pivchallenge.org/pivchallenge4.html#case_d. \ No newline at end of file diff --git a/openpiv/docs/conf.py b/openpiv/docs/conf.py index 0205c70e..106db437 100644 --- a/openpiv/docs/conf.py +++ b/openpiv/docs/conf.py @@ -80,7 +80,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ['_build', '**.ipynb_checkpoints'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None diff --git a/openpiv/test/test_calib_dlt.py b/openpiv/test/test_calib_dlt.py new file mode 100644 index 00000000..4ccee6f7 --- /dev/null +++ b/openpiv/test/test_calib_dlt.py @@ -0,0 +1,419 @@ +import os +import numpy as np +import pytest + +from numpy.testing import (assert_equal, assert_allclose, + assert_almost_equal, assert_array_almost_equal, + assert_array_equal, assert_) + +from openpiv.calibration import dlt_model +from openpiv.calibration.calib_utils import get_reprojection_error, get_los_error + +file = os.path.join(os.path.dirname(__file__),"test_calibration_points.npz") + +def test_parameters_input(): + with pytest.raises(TypeError): + # missing camera name + dlt_model.camera() + + # missing resolution + dlt_model.camera( + "name" + ) + + with pytest.raises(ValueError): + # name is not a string + dlt_model.camera( + 0, + resolution=[0, 0] + ) + + # not two element tuple + dlt_model.camera( + "name", + resolution=[0] + ) + + # not 2D or 3D + dlt_model.camera( + "name", + resolution=[0, 0], + ndim = 4 + ) + + # coefficients not correct dimension + dlt_model.camera( + "name", + resolution=[0, 0], + coeffs = np.zeros((10, 10, 2)) + ) + + # coefficients not correct shape + dlt_model.camera( + "name", + resolution=[0, 0], + coeffs = np.zeros((10, 10)) + ) + + +def test_parameters_initialization(): + cam = dlt_model.camera( + "name", + resolution=[0, 0] + ) + + assert_(hasattr(cam, "name")) + assert_(hasattr(cam, "resolution")) + assert_(hasattr(cam, "coeffs")) + assert_(hasattr(cam, "dtype")) + + assert_(len(cam.resolution) == 2) + + assert_(cam.coeffs.shape in [(3, 3), (3, 4)]) + + assert_(cam.dtype in ["float32", "float64"]) + + +@pytest.mark.parametrize("case", (1, 2, 3)) +def test_minimization_01( + case: int +): + cal_data = np.load(file) + + cal_obj_points = cal_data[f"obj_points_{case}"] + cal_img_points = cal_data[f"img_points_{case}"] + + cam = dlt_model.camera( + "dummy", + resolution = [512, 512], + ) + + cam.minimize_params( + cal_obj_points, + cal_img_points + ) + + RMSE = get_reprojection_error( + cam, + cal_obj_points, + cal_img_points + ) + + assert_(RMSE < 1e-2) + + +@pytest.mark.parametrize("case", (1, 2, 3)) +def test_projection_01( + case: int +): + cal_data = np.load(file) + + cal_obj_points = cal_data[f"obj_points_{case}"] + cal_img_points = cal_data[f"img_points_{case}"] + + cam = dlt_model.camera( + "dummy", + resolution = [512, 512], + ) + + cam.minimize_params( + cal_obj_points, + cal_img_points + ) + + obj_points = np.random.rand(3, 32) + obj_points[0, :] = np.int32(obj_points[0, :] * 50) + obj_points[1, :] = np.int32(obj_points[1, :] * 50) + obj_points[2, :] = np.int32(obj_points[2, :] * 10) + + obj_points = obj_points.astype("float64", copy=False) + + img_points = cam.project_points( + obj_points + ) + + recon_obj_points = cam.project_to_z( + img_points, + obj_points[2] + ) + + assert_array_almost_equal( + obj_points, + recon_obj_points, + decimal=2 + ) + + +@pytest.mark.parametrize("case", (1, 2, 3)) +def test_projection_02( + case: int +): + cal_data = np.load(file) + + cal_obj_points = cal_data[f"obj_points_{case}"] + cal_img_points = cal_data[f"img_points_{case}"] + + cam = dlt_model.camera( + "dummy", + resolution = [512, 512], + ) + + cam.minimize_params( + cal_obj_points, + cal_img_points + ) + + x, y = cam.project_points( + cal_obj_points + ) + + assert_array_almost_equal( + x, cal_img_points[0], + decimal=2 + ) + + assert_array_almost_equal( + y, cal_img_points[1], + decimal=2 + ) + + +@pytest.mark.parametrize("case", (1, 2, 3)) +def test_projection_03( + case: int +): + cal_data = np.load(file) + + cal_obj_points = cal_data[f"obj_points_{case}"] + cal_img_points = cal_data[f"img_points_{case}"] + + cam = dlt_model.camera( + "dummy", + resolution = [512, 512], + ) + + cam.minimize_params( + cal_obj_points, + cal_img_points + ) + + RMSE_0 = get_los_error( + cam, + z = -10 + ) + + RMSE_1 = get_los_error( + cam, + z = 0 + ) + + RMSE_2 = get_los_error( + cam, + z = 10 + ) + + assert_(RMSE_0 < 1e-2) + assert_(RMSE_1 < 1e-2) + assert_(RMSE_2 < 1e-2) + + +@pytest.mark.parametrize("case", ((1, 2), (1, 3), (2, 3))) +def test_line_intersect_01( + case: tuple +): + cal_data = np.load(file) + + cal_obj_points_1 = cal_data[f"obj_points_{case[0]}"] + cal_img_points_1 = cal_data[f"img_points_{case[0]}"] + + cal_obj_points_2 = cal_data[f"obj_points_{case[1]}"] + cal_img_points_2 = cal_data[f"img_points_{case[1]}"] + + cam_1 = dlt_model.camera( + "dummy", + resolution = [512, 512] + ) + + cam_2 = dlt_model.camera( + "dummy", + resolution = [512, 512] + ) + + cam_1.minimize_params( + cal_obj_points_1, + cal_img_points_1 + ) + + cam_2.minimize_params( + cal_obj_points_2, + cal_img_points_2 + ) + + test_object_points = np.array( + [ + [0, 0, 0], + [1, 2, 3], + [3, 2, 1], + [15, 15, -15], + [15, 15, 0], + [15, 15, 15] + ], + dtype=cam_1.dtype + ).T + + test_image_points_1 = cam_1.project_points( + test_object_points + ) + + test_image_points_2 = cam_2.project_points( + test_object_points + ) + + recon_obj_points, _ = dlt_model.line_intersect( + cam_1, + cam_2, + test_image_points_1, + test_image_points_2 + ) + + assert_array_almost_equal( + test_object_points, + recon_obj_points, + decimal=2 + ) + + +@pytest.mark.parametrize("case", ((1, 2), (1, 3), (2, 3), (1, 2, 3))) +def test_line_intersect_02( + case: tuple +): + cal_data = np.load(file) + n_cams = len(case) + + cal_obj_points = [] + cal_img_points = [] + for cam in case: + cal_obj_points.append(cal_data[f"obj_points_{cam}"]) + cal_img_points.append(cal_data[f"img_points_{cam}"]) + + cams = [] + for cam in range(n_cams): + cam_1 = dlt_model.camera( + "dummy", + resolution = [512, 512] + ) + + cam_1.minimize_params( + cal_obj_points[cam], + cal_img_points[cam] + ) + + cams.append(cam_1) + + test_object_points = np.array( + [ + [0, 0, 0], + [1, 2, 3], + [3, 2, 1], + [15, 15, -15], + [15, 15, 0], + [15, 15, 15] + ], + dtype=cam_1.dtype + ).T + + test_image_points = [] + for cam in range(n_cams): + test_image_points.append( + cams[cam].project_points( + test_object_points + ) + ) + + recon_obj_points = dlt_model.multi_line_intersect( + cams, + test_image_points + ) + + assert_array_almost_equal( + test_object_points, + recon_obj_points, + decimal=0 + ) + + +def test_save_parameters_1(): + cam = dlt_model.camera( + "dummy", + resolution = [512, 512] + ) + + cam.save_parameters( + "." + ) + + +def test_save_parameters_2(): + cam = dlt_model.camera( + "dummy", + resolution = [512, 512] + ) + + cam.save_parameters( + ".", "saved_params" + ) + + +def test_load_parameters_1(): + cam = dlt_model.camera( + "dummy", + resolution = [512, 512] + ) + + with pytest.raises(FileNotFoundError): + cam.load_parameters( + ".", + "does not exist (hopefully)" + ) + + +def test_load_parameters_2(): + cam_orig = dlt_model.camera( + "dummy", + resolution = [512, 512] + ) + + cam_new = dlt_model.camera( + "dummy", + resolution = [512, 512] + ) + + cam_orig.save_parameters( + ".", + "dummy" + ) + + cam_new.load_parameters( + ".", + "dummy" + ) + + assert_array_equal( + cam_orig.name, + cam_new.name + ) + + assert_array_equal( + cam_orig.resolution, + cam_new.resolution + ) + + assert_array_equal( + cam_orig.coeffs, + cam_new.coeffs + ) + + assert_array_equal( + cam_orig.dtype, + cam_new.dtype + ) \ No newline at end of file diff --git a/openpiv/test/test_calib_pinhole.py b/openpiv/test/test_calib_pinhole.py new file mode 100644 index 00000000..c63ac65e --- /dev/null +++ b/openpiv/test/test_calib_pinhole.py @@ -0,0 +1,677 @@ +import os +import numpy as np +import pytest + +from numpy.testing import (assert_equal, assert_allclose, + assert_almost_equal, assert_array_almost_equal, + assert_array_equal, assert_) + +from openpiv.calibration import pinhole_model +from openpiv.calibration.calib_utils import get_reprojection_error, get_los_error +file = os.path.join(os.path.dirname(__file__),"test_calibration_points.npz") + + +def get_test_camera_params( + case: int=1 +): + pinhole_cam = os.path.join(os.path.dirname(__file__), f"test_calibration_pinhole_{case}.txt") + + + with open(pinhole_cam, 'r') as f: + name = f.readline()[:-1] + + line = f.readline()[:] + resolution = [float(num) for num in line.split()] + + line = f.readline()[:] + translation = [float(num) for num in line.split()] + + line = f.readline()[:] + orientation = [float(num) for num in line.split()] + + line = f.readline()[:] + distortion = [float(num) for num in line.split()] + + line = f.readline()[:] + focal = [float(num) for num in line.split()] + + line = f.readline()[:] + principal = [float(num) for num in line.split()] + + camera = pinhole_model.camera( + name = name, + resolution = resolution, + translation = translation, + orientation = orientation, + distortion_model = "brown", + distortion1 = distortion, + focal = focal, + principal = principal + ) + + return camera + + +def test_parameters_input(): + with pytest.raises(TypeError): + # missing camera name + pinhole_model.camera() + + # missing resolution + pinhole_model.camera( + "name" + ) + + with pytest.raises(ValueError): + # name is not a string + pinhole_model.camera( + 0, + resolution=[0, 0] + ) + + # not two element tuple + pinhole_model.camera( + "name", + resolution=[0] + ) + + # not three element vector + pinhole_model.camera( + "name", + resolution=[0, 0], + translation=[0, 0] + ) + + # not three element vector + pinhole_model.camera( + "name", + resolution=[0, 0], + orientation=[0, 0] + ) + + # wrong distortion model + pinhole_model.camera( + "name", + resolution=[0, 0], + distortion_model="non-existent ", + ) + + # not 8 element vector for brown model + pinhole_model.camera( + "name", + resolution=[0, 0], + distortion_model="brown", + distortion1=np.zeros(7) + ) + + # not 2 x 5 matrix for polynomial model + pinhole_model.camera( + "name", + resolution=[0, 0], + distortion_model="polynomial", + distortion2=np.zeros([4, 3]) + ) + + # not 2 element list-like + pinhole_model.camera( + "name", + resolution=[0, 0], + focal="[2, 2]" + ) + + # not 2 element vector + pinhole_model.camera( + "name", + resolution=[0, 0], + focal=[1] + ) + + # not 2 element list-like + pinhole_model.camera( + "name", + resolution=[0, 0], + principal="[2, 2]" + ) + + # not 2 element vector + pinhole_model.camera( + "name", + resolution=[0, 0], + principal=[1] + ) + + # not a support dtype (supported dtypes are float32 and float64) + pinhole_model.camera( + "name", + resolution=[0, 0], + dtype=int + ) + + +def test_parameters_initialization(): + cam = pinhole_model.camera( + "name", + resolution=[0, 0] + ) + + assert_(hasattr(cam, "name")) + assert_(hasattr(cam, "resolution")) + assert_(hasattr(cam, "translation")) + assert_(hasattr(cam, "orientation")) + assert_(hasattr(cam, "rotation")) + assert_(hasattr(cam, "distortion_model")) + assert_(hasattr(cam, "distortion1")) + assert_(hasattr(cam, "distortion2")) + assert_(hasattr(cam, "focal")) + assert_(hasattr(cam, "principal")) + assert_(hasattr(cam, "dtype")) + + assert_(len(cam.resolution) == 2) + + assert_equal( + cam.translation.shape, + [3, ] + ) + + assert_equal( + cam.orientation.shape, + [3, ] + ) + + assert_equal( + cam.rotation.shape, + [3, 3] + ) + + assert_equal( + cam.distortion1.shape, + [8, ] + ) + + assert_equal( + cam.distortion2.shape, + [2, 5] + ) + + assert_(len(cam.focal) == 2) + + assert_(len(cam.principal) == 2) + + # float32 does not work well with the pinhole model, should it be deprecated? + assert_(cam.dtype in ["float32", "float64"]) + + +def test_rotation_matrix_01(): + cam = pinhole_model.camera( + "name", + resolution = [1024, 1024] + ) + + assert_allclose( + cam.rotation, + np.eye(3,3) + ) + + +def test_rotation_matrix_02(): + cam = pinhole_model.camera( + "name", + resolution = [1024, 1024], + translation = [0, 0, 1], + orientation = [0, 0, 0] + ) + + assert_allclose( + cam.rotation, + np.eye(3,3) + ) + + +def test_projection_01(): + cam = pinhole_model.camera( + "name", + resolution = [1024, 1024] + ) + + X, Y, Z = np.random.rand(3, 32) * 100.0 + + x, y = cam.project_points( + [X, Y, Z] + ) + + X_new, Y_new, Z_new = cam.project_to_z( + [x, y], + Z + ) + + assert_array_almost_equal( + X, X_new, + decimal=4 + ) + + assert_array_almost_equal( + Y, Y_new, + decimal=4 + ) + + assert_array_almost_equal( + Z, Z_new, + decimal=4 + ) + + +@pytest.mark.parametrize("case", (1, 2, 3)) +def test_projection_02( + case: int +): + cam = get_test_camera_params(case) + + cal_data = np.load(file) + + cal_obj_points = cal_data[f"obj_points_{case}"] + cal_img_points = cal_data[f"img_points_{case}"] + + x, y = cam.project_points( + cal_obj_points + ) + + assert_array_almost_equal( + x, cal_img_points[0], + decimal=2 + ) + + assert_array_almost_equal( + y, cal_img_points[1], + decimal=2 + ) + + +@pytest.mark.parametrize("case", (1, 2, 3)) +def test_projection_03( + case: int +): + cam = get_test_camera_params(case) + + cal_data = np.load(file) + + cal_obj_points = cal_data[f"obj_points_{case}"] + cal_img_points = cal_data[f"img_points_{case}"] + + RMSE = get_reprojection_error( + cam, + cal_obj_points, + cal_img_points + ) + + assert_(RMSE < 1e-2) + + +@pytest.mark.parametrize("case", (1, 2, 3)) +def test_projection_04( + case: int +): + cam = get_test_camera_params(case) + + cal_data = np.load(file) + + cal_obj_points = cal_data[f"obj_points_{case}"] + cal_img_points = cal_data[f"img_points_{case}"] + + RMSE_0 = get_los_error( + cam, + z = -10 + ) + + RMSE_1 = get_los_error( + cam, + z = 0 + ) + + RMSE_2 = get_los_error( + cam, + z = 10 + ) + + assert_(RMSE_0 < 1e-2) + assert_(RMSE_1 < 1e-2) + assert_(RMSE_2 < 1e-2) + + +@pytest.mark.parametrize("case", (1, 2, 3)) +def test_projection_05( + case: int +): + cam = get_test_camera_params(case) + + cal_data = np.load(file) + + cal_obj_points = cal_data[f"obj_points_{case}"] + cal_img_points = cal_data[f"img_points_{case}"] + + X, Y, Z = cam.project_to_z( + cal_img_points, + cal_obj_points[2] + ) + + assert_array_almost_equal( + X, cal_obj_points[0], + decimal=2 + ) + + assert_array_almost_equal( + Y, cal_obj_points[1], + decimal=2 + ) + + assert_array_almost_equal( + Z, cal_obj_points[2], + decimal=2 + ) + + +@pytest.mark.parametrize("model", ("brown", "polynomial")) +def test_minimization_01( + model: str +): + cal_data = np.load(file) + + cal_obj_points = cal_data["obj_points_1"] + cal_img_points = cal_data["img_points_1"] + + cam = pinhole_model.camera( + "minimized", + resolution = [512, 512], + translation = [1, 1, 520], # initial guess + orientation = [0, np.pi, np.pi], # initial guess + focal = [1000, 1000], + distortion_model = model + ) + + cam.minimize_params( + cal_obj_points, + cal_img_points, + correct_focal = False, + correct_distortion = False, + iterations = 3 + ) + + RMSE = get_reprojection_error( + cam, + cal_obj_points, + cal_img_points + ) + + assert_(RMSE < 1e-2) + + +@pytest.mark.parametrize("model", ("brown", "polynomial")) +def test_minimization_02( + model: str +): + case = 1 + cam_orig = get_test_camera_params(case) + + cal_data = np.load(file) + + cal_obj_points = cal_data[f"obj_points_{case}"] + cal_img_points = cal_data[f"img_points_{case}"] + + cam_new = pinhole_model.camera( + "minimized", + resolution = [512, 512], + translation = [-20, 20, 520], # initial guess + orientation = [0, np.pi, np.pi], # initial guess + focal = [1000, 1000], + distortion_model = model + ) + + cam_new.minimize_params( + cal_obj_points, + cal_img_points, + correct_focal = False, + correct_distortion = False, + iterations = 3 + ) + + assert_array_almost_equal( + cam_orig.translation, + cam_new.translation, + decimal = 1 + ) + + assert_array_almost_equal( + cam_orig.orientation, + cam_new.orientation, + decimal = 1 + ) + + assert_array_almost_equal( + cam_orig.rotation, + cam_new.rotation, + decimal = 1 + ) + + assert_array_almost_equal( + cam_orig.focal, + cam_new.focal, + decimal = 1 + ) + + # set decimal to 0 because it keeps minimizing wrong, this needs to be fixed + assert_array_almost_equal( + cam_orig.principal, + cam_new.principal, + decimal = 0 + ) + + +@pytest.mark.parametrize("case", ((1, 2), (1, 3), (2, 3))) +def test_line_intersect_01( + case: tuple +): + cal_data = np.load(file) + + cal_obj_points_1 = cal_data[f"obj_points_{case[0]}"] + cal_img_points_1 = cal_data[f"img_points_{case[1]}"] + + cal_obj_points_2 = cal_data[f"obj_points_{case[0]}"] + cal_img_points_2 = cal_data[f"img_points_{case[1]}"] + + cam_1 = get_test_camera_params(case[0]) + + cam_2 = get_test_camera_params(case[1]) + + test_object_points = np.array( + [ + [0, 0, 0], + [1, 2, 3], + [3, 2, 1], + [15, 15, -15], + [15, 15, 0], + [15, 15, 15] + ], + dtype=cam_1.dtype + ).T + + test_image_points_1 = cam_1.project_points( + test_object_points + ) + + test_image_points_2 = cam_2.project_points( + test_object_points + ) + + recon_obj_points, _ = pinhole_model.line_intersect( + cam_1, + cam_2, + test_image_points_1, + test_image_points_2 + ) + + assert_array_almost_equal( + test_object_points, + recon_obj_points, + decimal=2 + ) + + +@pytest.mark.parametrize("case", ((1, 2), (1, 3), (2, 3), (1, 2, 3))) +def test_line_intersect_02( + case: tuple +): + cal_data = np.load(file) + n_cams = len(case) + + cal_obj_points = [] + cal_img_points = [] + for cam in case: + cal_obj_points.append(cal_data[f"obj_points_{cam}"]) + cal_img_points.append(cal_data[f"img_points_{cam}"]) + + cams = [] + for cam in case: + cams.append(get_test_camera_params(cam)) + + test_object_points = np.array( + [ + [0, 0, 0], + [1, 2, 3], + [3, 2, 1], + [15, 15, -15], + [15, 15, 0], + [15, 15, 15] + ], + dtype=cams[0].dtype + ).T + + test_image_points = [] + for cam in range(n_cams): + test_image_points.append( + cams[cam].project_points( + test_object_points + ) + ) + + recon_obj_points = pinhole_model.multi_line_intersect( + cams, + test_image_points + ) + + assert_array_almost_equal( + test_object_points, + recon_obj_points, + decimal=2 + ) + + +def test_save_parameters_1(): + cam = get_test_camera_params() + + cam.save_parameters( + "." + ) + + +@pytest.mark.parametrize("default", (True, False)) +def test_save_parameters_2( + default: bool +): + + if default: + cam = pinhole_model.camera( + "dummy", + resolution = [512, 512] + ) + else: + cam = get_test_camera_params() + + cam.save_parameters( + ".", "saved_params" + ) + + +def test_load_parameters_1(): + cam = pinhole_model.camera( + "dummy", + resolution = [512, 512] + ) + + with pytest.raises(FileNotFoundError): + cam.load_parameters( + ".", + "does not exist (hopefully)" + ) + + +def test_load_parameters_2(): + cam_orig = get_test_camera_params() + + + cam_orig.save_parameters( + ".", + "dummy" + ) + + cam_new = pinhole_model.camera( + "dummy", + resolution = [512, 512] + ) + + cam_new.load_parameters( + ".", + "dummy" + ) + assert_array_equal( + cam_orig.name, + cam_new.name + ) + + assert_array_equal( + cam_orig.resolution, + cam_new.resolution + ) + + assert_array_equal( + cam_orig.translation, + cam_new.translation + ) + + assert_array_equal( + cam_orig.orientation, + cam_new.orientation + ) + + assert_array_equal( + cam_orig.rotation, + cam_new.rotation + ) + + assert_array_equal( + cam_orig.distortion_model, + cam_new.distortion_model + ) + + assert_array_equal( + cam_orig.distortion1, + cam_new.distortion1 + ) + + assert_array_equal( + cam_orig.distortion2, + cam_new.distortion2 + ) + + assert_array_equal( + cam_orig.focal, + cam_new.focal + ) + + assert_array_equal( + cam_orig.principal, + cam_new.principal + ) + + assert_array_equal( + cam_orig.dtype, + cam_new.dtype + ) \ No newline at end of file diff --git a/openpiv/test/test_calib_point_matching.py b/openpiv/test/test_calib_point_matching.py new file mode 100644 index 00000000..b2a2e850 --- /dev/null +++ b/openpiv/test/test_calib_point_matching.py @@ -0,0 +1,126 @@ +import os +import numpy as np +import pytest + +from numpy.testing import (assert_equal, assert_allclose, + assert_almost_equal, assert_array_almost_equal, + assert_array_equal, assert_) + +from openpiv.calibration import calib_utils + + +def test_find_nearest_points_01(): + grid = calib_utils.get_simple_grid( + 10, 10, + 0, 0, 0, + spacing=10, + flip_y=False + )[:2] + + selected_points = np.array( + [ + [1, 1], + [2, 2], + [4, 4], + [6, 6], + [93, 93], + [110, 110] + ], + dtype="float64" + ).T + + expected_points = np.array( + [ + [0, 0], + [0, 0], + [0, 0], + [10, 10], + [90, 90], + [90, 90] + ], + dtype="float64" + ).T + + # without NaNs threshold + found_points = calib_utils.find_nearest_points( + grid, + selected_points, + threshold=None, + flag_nans=False + ) + + assert_array_equal( + expected_points, + found_points + ) + + # with NaNs threshold + found_points = calib_utils.find_nearest_points( + grid, + selected_points, + threshold=5, + flag_nans=True + ) + + assert_(np.isnan(found_points[0, 2])) + assert_(np.isnan(found_points[0, 3])) + assert_(np.isnan(found_points[0, 5])) + + assert_(np.isnan(found_points[1, 2])) + assert_(np.isnan(found_points[1, 3])) + assert_(np.isnan(found_points[1, 5])) + + nan_mask = ~np.isnan(found_points[0,:]) + + found_points = found_points[:, nan_mask] + expected_points = expected_points[:, nan_mask] + + assert_array_equal( + expected_points, + found_points + ) + + +def test_find_nearest_points_02(): + grid = calib_utils.get_simple_grid( + 10, 10, + 0, 0, 0, + spacing=10, + flip_y=False + )[:2] + + selected_points = np.array( + [ + [1, 1], + [2, 2], + [4, 4], + [6, 6], + [93, 93], + [110, 110] + ], + dtype="float64" + ).T + + expected_points = np.array( + [ + [0, 0], + [0, 0], +# [0, 0], # out of bound of threshold +# [10, 10], # out of bound of threshold + [90, 90], +# [90, 90] # out of bound of threshold + ], + dtype="float64" + ).T + + found_points = calib_utils.find_nearest_points( + grid, + selected_points, + threshold=5.5, + flag_nans=False + ) + + assert_array_equal( + expected_points, + found_points + ) \ No newline at end of file diff --git a/openpiv/test/test_calib_polynomial.py b/openpiv/test/test_calib_polynomial.py new file mode 100644 index 00000000..b44adccf --- /dev/null +++ b/openpiv/test/test_calib_polynomial.py @@ -0,0 +1,376 @@ +import os +import numpy as np +import pytest + +from numpy.testing import (assert_equal, assert_allclose, + assert_almost_equal, assert_array_almost_equal, + assert_array_equal, assert_) + +from openpiv.calibration import poly_model +from openpiv.calibration.calib_utils import get_reprojection_error, get_los_error + +file = os.path.join(os.path.dirname(__file__),"test_calibration_points.npz") + +def test_parameters_input(): + with pytest.raises(TypeError): + # missing camera name + poly_model.camera() + + # missing resolution + poly_model.camera( + "name" + ) + + with pytest.raises(ValueError): + # name is not a string + poly_model.camera( + 0, + resolution=[0, 0] + ) + + # not two element tuple + poly_model.camera( + "name", + resolution=[0] + ) + + # not 2D + poly_model.camera( + "name", + resolution=[0, 0], + poly_wi = np.zeros(19) + ) + + # not 2D + poly_model.camera( + "name", + resolution=[0, 0], + poly_iw = np.zeros(19) + ) + + # not correct shape + poly_model.camera( + "name", + resolution=[0, 0], + poly_wi = np.zeros((10, 10)) + ) + + # not correct shape + poly_model.camera( + "name", + resolution=[0, 0], + poly_iw = np.zeros((10, 10)) + ) + + +def test_parameters_initialization(): + cam = poly_model.camera( + "name", + resolution=[0, 0] + ) + + assert_(hasattr(cam, "name")) + assert_(hasattr(cam, "resolution")) + assert_(hasattr(cam, "poly_wi")) + assert_(hasattr(cam, "poly_iw")) + assert_(hasattr(cam, "dlt")) + assert_(hasattr(cam, "dtype")) + + assert_(len(cam.resolution) == 2) + + assert_equal( + cam.poly_wi.shape, + [19, 2] + ) + + assert_equal( + cam.poly_iw.shape, + [19, 3] + ) + + assert_(cam.dtype in ["float32", "float64"]) + + +@pytest.mark.parametrize("case", (1, 2, 3)) +def test_minimization_01( + case: int +): + cal_data = np.load(file) + + cal_obj_points = cal_data[f"obj_points_{case}"] + cal_img_points = cal_data[f"img_points_{case}"] + + cam = poly_model.camera( + "poly", + resolution = [512, 512], + ) + + cam.minimize_params( + cal_obj_points, + cal_img_points + ) + + RMSE = get_reprojection_error( + cam, + cal_obj_points, + cal_img_points + ) + + assert_(RMSE < 1e-2) + + +@pytest.mark.parametrize("case", (1, 2)) +def test_projection_01( + case: int +): + cal_data = np.load(file) + + cal_obj_points = cal_data[f"obj_points_{case}"] + cal_img_points = cal_data[f"img_points_{case}"] + + cam = poly_model.camera( + "poly", + resolution = [512, 512], + ) + + cam.minimize_params( + cal_obj_points, + cal_img_points + ) + + obj_points = np.random.rand(3, 32) + obj_points[0, :] = np.int32(obj_points[0, :] * 50) + obj_points[1, :] = np.int32(obj_points[1, :] * 50) + obj_points[2, :] = np.int32(obj_points[2, :] * 10) + + obj_points = obj_points.astype("float64", copy=False) + + img_points = cam.project_points( + obj_points + ) + + recon_obj_points = cam.project_to_z( + img_points, + obj_points[2] + ) + + assert_array_almost_equal( + obj_points, + recon_obj_points, + decimal=2 + ) + + +@pytest.mark.parametrize("case", (1, 2, 3)) +def test_projection_02( + case: int +): + cal_data = np.load(file) + + cal_obj_points = cal_data[f"obj_points_{case}"] + cal_img_points = cal_data[f"img_points_{case}"] + + cam = poly_model.camera( + "poly", + resolution = [512, 512], + ) + + cam.minimize_params( + cal_obj_points, + cal_img_points + ) + + x, y = cam.project_points( + cal_obj_points + ) + + assert_array_almost_equal( + x, cal_img_points[0], + decimal=2 + ) + + assert_array_almost_equal( + y, cal_img_points[1], + decimal=2 + ) + +# Test case 1 and 3 needs higher thresholds due to camera malignment +@pytest.mark.parametrize("case", (1, 2, 3)) +def test_projection_03( + case: int +): + cal_data = np.load(file) + + cal_obj_points = cal_data[f"obj_points_{case}"] + cal_img_points = cal_data[f"img_points_{case}"] + + cam = poly_model.camera( + "poly", + resolution = [512, 512], + ) + + cam.minimize_params( + cal_obj_points, + cal_img_points + ) + + RMSE_0 = get_los_error( + cam, + z = -10 + ) + + RMSE_1 = get_los_error( + cam, + z = 0 + ) + + RMSE_2 = get_los_error( + cam, + z = 10 + ) + + assert_(RMSE_0 < 0.5) + assert_(RMSE_1 < 0.5) + assert_(RMSE_2 < 0.5) + + +@pytest.mark.parametrize("case", ((1, 2), (1, 3), (2, 3), (1, 2, 3))) +def test_line_intersect_01( + case: tuple +): + cal_data = np.load(file) + n_cams = len(case) + + cal_obj_points = [] + cal_img_points = [] + for cam in case: + cal_obj_points.append(cal_data[f"obj_points_{cam}"]) + cal_img_points.append(cal_data[f"img_points_{cam}"]) + + cams = [] + for cam in range(n_cams): + cam_1 = poly_model.camera( + "dummy", + resolution = [512, 512] + ) + + cam_1.minimize_params( + cal_obj_points[cam], + cal_img_points[cam] + ) + + cams.append(cam_1) + + test_object_points = np.array( + [ + [0, 0, 0], + [1, 2, 3], + [3, 2, 1], + [15, 15, -15], + [15, 15, 0], + [15, 15, 15] + ], + dtype=cam_1.dtype + ).T + + test_image_points = [] + for cam in range(n_cams): + test_image_points.append( + cams[cam].project_points( + test_object_points + ) + ) + + recon_obj_points = poly_model.multi_line_intersect( + cams, + test_image_points + ) + + assert_array_almost_equal( + test_object_points, + recon_obj_points, + decimal=0 + ) + + +def test_save_parameters_1(): + cam = poly_model.camera( + "dummy", + resolution = [512, 512] + ) + + cam.save_parameters( + "." + ) + + +def test_save_parameters_2(): + cam = poly_model.camera( + "dummy", + resolution = [512, 512] + ) + + + cam.save_parameters( + ".", "saved_params" + ) + + +def test_load_parameters_1(): + cam = poly_model.camera( + "dummy", + resolution = [512, 512] + ) + + with pytest.raises(FileNotFoundError): + cam.load_parameters( + ".", + "does not exist (hopefully)" + ) + + +def test_load_parameters_2(): + cam_orig = poly_model.camera( + "dummy", + resolution = [512, 512] + ) + + cam_new = poly_model.camera( + "dummy", + resolution = [512, 512] + ) + + cam_orig.save_parameters( + ".", + "dummy" + ) + + cam_new.load_parameters( + ".", + "dummy" + ) + + assert_array_equal( + cam_orig.name, + cam_new.name + ) + + assert_array_equal( + cam_orig.resolution, + cam_new.resolution + ) + + assert_array_equal( + cam_orig.poly_wi, + cam_new.poly_wi + ) + + assert_array_equal( + cam_orig.poly_iw, + cam_new.poly_iw + ) + + assert_array_equal( + cam_orig.dtype, + cam_new.dtype + ) \ No newline at end of file diff --git a/openpiv/test/test_calib_target_grids.py b/openpiv/test/test_calib_target_grids.py new file mode 100644 index 00000000..3548b900 --- /dev/null +++ b/openpiv/test/test_calib_target_grids.py @@ -0,0 +1,83 @@ +import os +import numpy as np +import pytest + +from numpy.testing import (assert_equal, assert_allclose, + assert_almost_equal, assert_array_almost_equal, + assert_array_equal, assert_) + +from openpiv.calibration import poly_model +from openpiv.calibration.calib_utils import get_simple_grid, get_asymmetric_grid + + +def test_simple_grid_01(): + grid_shape = (9, 9) + origin = np.array(grid_shape) // 2 + z_plane = 0 + spacing = 10 + + grid = get_simple_grid( + grid_shape[0], grid_shape[1], + z_plane, + origin[0], origin[1], + spacing=spacing + ) + + # check horizontal and vertical spacing + assert_((grid[0, 1] - grid[0, 0]) == spacing) + assert_((grid[1, 1] - grid[1, grid_shape[1]]) == spacing) + + # make sure z-plane is correct + assert_(np.all(grid[2, :] == z_plane)) + + # check grid shape + assert_equal( + grid.shape, + (3, np.prod(grid_shape)) + ) + + # make sure origin is 0 + grid = grid.reshape((3, grid_shape[0], grid_shape[1])) + + origin = grid[:2, origin[0], origin[1]] + + assert_(origin[0] == 0) + assert_(origin[1] == 0) + + +def test_asymmetric_grid_01(): + grid_shape = (9, 9) + origin = np.array(grid_shape) // 2 + + z_plane = 0 + spacing = 10 + + grid = get_asymmetric_grid( + grid_shape[0], grid_shape[1], + z_plane, + origin[0], origin[1], + spacing=spacing + ) + + # check horizontal and vertical spacing + assert_((grid[0, 1] - grid[0, 0]) == (spacing * 2)) + assert_((grid[1, 1] - grid[1, grid_shape[1]]) == (spacing * 2)) + + # make sure z-plane is correct + assert_(np.all(grid[2, :] == z_plane)) + + # check grid shape + grid_length = int(np.prod(grid_shape) / 2 + 0.5) + + assert_equal( + grid.shape, + (3, grid_length) + ) + + # make sure origin is 0 + origin_ind = int(grid_length / 2) + + origin = grid[:2, origin_ind] + + assert_(origin[0] == 0) + assert_(origin[1] == 0) \ No newline at end of file diff --git a/openpiv/test/test_calib_utils.py b/openpiv/test/test_calib_utils.py new file mode 100644 index 00000000..b0686a3e --- /dev/null +++ b/openpiv/test/test_calib_utils.py @@ -0,0 +1,135 @@ +import numpy as np +import os +import pytest + +from numpy.testing import (assert_equal, assert_allclose, + assert_almost_equal, assert_array_almost_equal, + assert_) + +from openpiv.calibration import calib_utils +from openpiv.calibration import dlt_model + + +file = os.path.join(os.path.dirname(__file__),"test_calibration_points.npz") + + +# this function is not used yet, but will be when more utility functions are tested +def get_camera_instance(): + cal_data = np.load(file) + + case = 1 + + cal_obj_points = cal_data[f"obj_points_{case}"] + cal_img_points = cal_data[f"img_points_{case}"] + + cam = dlt_model.camera( + "dummy", + resolution = [512, 512], + ) + + cam.minimize_params( + cal_obj_points, + cal_img_points + ) + + return cam + + +def test_homogenize_01(): + shape = (2, 24) + expected = (shape[0] + 1, shape[1]) + + img_points = np.random.rand(shape[0], shape[1]) + + homogenized = calib_utils.homogenize(img_points) + + assert_equal( + homogenized.shape, + expected + ) + assert_(np.all(homogenized[2, :] == 1)) + + +def test_get_rmse_01(): + shape = 24 + offset = 1.5 + + test_array = np.zeros(shape, dtype = "float64") + + error = (test_array + offset) - test_array + + rmse = calib_utils.get_rmse(error) + + assert_equal( + rmse, + offset + ) + + +def test_get_rmse_02(): + shape = (2, 24) + offset = 2 + expected = np.sqrt(np.power(2, 3)) + + test_array = np.zeros(shape, dtype = "float64") + + error = (test_array + offset) - test_array + + rmse = calib_utils.get_rmse(error) + + assert_equal( + rmse, + expected + ) + + +def test_get_rmse_03(): + shape = (2, 24, 5) + offset = 2 + expected = np.sqrt(np.power(2, 3)) + + test_array = np.zeros(shape, dtype = "float64") + + error = (test_array + offset) - test_array + + with pytest.raises(ValueError): + # invalid shape, get_rmse expects a 1D or 2D array + rmse = calib_utils.get_rmse(error) + + +def test_reprojection_error_01(): + cam = get_camera_instance() + + offset = 0.4 + + test_object_points = np.array( + [ + [0, 0, 0], + [1, 2, 3], + [3, 2, 1], + [15, 15, -15], + [15, 15, 0], + [15, 15, 15] + ], + dtype=cam.dtype + ).T + + test_image_points = cam.project_points(test_object_points) + + # Our definition of RMSE + error = test_image_points - (test_image_points + offset) + + expected = np.sqrt(np.mean(np.sum(np.square(error), 0))) + + # Now use the error function + results = calib_utils.get_reprojection_error( + cam, + test_object_points, + test_image_points + offset + ) + + assert_array_almost_equal( + expected, + results, + decimal=4 + ) \ No newline at end of file diff --git a/openpiv/test/test_calibration_pinhole_1.txt b/openpiv/test/test_calibration_pinhole_1.txt new file mode 100644 index 00000000..86c948a5 --- /dev/null +++ b/openpiv/test/test_calibration_pinhole_1.txt @@ -0,0 +1,7 @@ +minimized +512 512 +-60.95 42.11 535.14 +-0.003 2.860 3.037 +0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +1000 1000 +256.0 256.0 \ No newline at end of file diff --git a/openpiv/test/test_calibration_pinhole_2.txt b/openpiv/test/test_calibration_pinhole_2.txt new file mode 100644 index 00000000..8fdf829c --- /dev/null +++ b/openpiv/test/test_calibration_pinhole_2.txt @@ -0,0 +1,7 @@ +minimized +512 512 +48.91 83.24 563.35 +-0.083 3.094 3.184 +0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +1000 1000 +256.0 256.0 \ No newline at end of file diff --git a/openpiv/test/test_calibration_pinhole_3.txt b/openpiv/test/test_calibration_pinhole_3.txt new file mode 100644 index 00000000..01764414 --- /dev/null +++ b/openpiv/test/test_calibration_pinhole_3.txt @@ -0,0 +1,7 @@ +minimized +512 512 +131.26 35.2 550.2 +0.034 3.360 3.075 +0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +1000 1000 +256.0 256.0 \ No newline at end of file diff --git a/openpiv/test/test_calibration_points.npz b/openpiv/test/test_calibration_points.npz new file mode 100644 index 00000000..e8467910 Binary files /dev/null and b/openpiv/test/test_calibration_points.npz differ