def batch_of_rays(entry, exit): """ Definition to generate a batch of rays with given entry point(s) and exit point(s). Note that the mapping is one to one, meaning nth item in your entry points list will exit from nth item in your exit list and generate that particular ray. Note that you can have a combination like nx3 points for entry or exit and 1 point for entry or exit. But if you have multiple points both for entry and exit, the number of points have to be same both for entry and exit. Parameters ---------- entry : ndarray Either a single point with size of 3 or multiple points with the size of nx3. exit : ndarray Either a single point with size of 3 or multiple points with the size of nx3. Returns ---------- rays : ndarray Generated batch of rays. """ norays = np.array([0, 0]) if len(entry.shape) == 1: entry = entry.reshape((1, 3)) if len(exit.shape) == 1: exit = exit.reshape((1, 3)) norays = np.amax(np.asarray([entry.shape[0], exit.shape[0]])) if norays > exit.shape[0]: exit = np.repeat(exit, norays, axis=0) elif norays > entry.shape[0]: entry = np.repeat(entry, norays, axis=0) rays = [] norays = int(norays) for i in range(norays): rays.append(create_ray_from_two_points(entry[i], exit[i])) rays = np.asarray(rays) return rays
def read_PLY_point_cloud(filename): """ Definition to read a PLY file as a point cloud. Parameters ---------- filename : str Filename of a PLY file. Returns ---------- point_cloud : ndarray An array filled with poitns from the PLY file. """ plydata = PlyData.read(filename) if np.__name__ != 'numpy': import numpy as np_ply point_cloud = np_ply.zeros((plydata['vertex'][:].shape[0],3)) point_cloud[:,0] = np_ply.asarray(plydata['vertex']['x'][:]) point_cloud[:,1] = np_ply.asarray(plydata['vertex']['y'][:]) point_cloud[:,2] = np_ply.asarray(plydata['vertex']['z'][:]) point_cloud = np.asarray(point_cloud) else: point_cloud = np.zeros((plydata['vertex'][:].shape[0],3)) point_cloud[:,0] = np.asarray(plydata['vertex']['x'][:]) point_cloud[:,1] = np.asarray(plydata['vertex']['y'][:]) point_cloud[:,2] = np.asarray(plydata['vertex']['z'][:]) return point_cloud
def reflect(input_ray, normal): """ Definition to reflect an incoming ray from a surface defined by a surface normal. Used method described in G.H. Spencer and M.V.R.K. Murty, "General Ray-Tracing Procedure", 1961. Parameters ---------- input_ray : ndarray A vector/ray (2x3). It can also be a list of rays (nx2x3). normal : ndarray A surface normal (2x3). It also be a list of normals (nx2x3). Returns ---------- output_ray : ndarray Array that contains starting points and cosines of a reflected ray. """ input_ray = np.asarray(input_ray) normal = np.asarray(normal) if len(input_ray.shape) == 2: input_ray = input_ray.reshape((1, 2, 3)) if len(normal.shape) == 2: normal = normal.reshape((1, 2, 3)) mu = 1 div = normal[:, 1, 0]**2 + normal[:, 1, 1]**2 + normal[:, 1, 2]**2 a = mu * (input_ray[:, 1, 0] * normal[:, 1, 0] + input_ray[:, 1, 1] * normal[:, 1, 1] + input_ray[:, 1, 2] * normal[:, 1, 2]) / div n = np.int(np.amax(np.array([normal.shape[0], input_ray.shape[0]]))) output_ray = np.zeros((n, 2, 3)) output_ray[:, 0] = normal[:, 0] output_ray[:, 1] = input_ray[:, 1] - 2 * a * normal[:, 1] if output_ray.shape[0] == 1: output_ray = output_ray.reshape((2, 3)) return output_ray
def nufft2(field, fx, fy, size=None, sign=1, eps=10**(-12)): """ """ if np.__name__ == 'cupy': fx = np.asnumpy(fx).astype(np.float64) fy = np.asnumpy(fy).astype(np.float64) image = np.asnumpy(np.copy(field)).astype(np.complex128) else: image = np.copy(field).astype(np.complex128) if type(size) == type(None): result = finufft.nufft2d1(fx.flatten(), fy.flatten(), image.flatten(), image.shape, eps=eps, isign=sign) else: result = finufft.nufft2d1(fx.flatten(), fy.flatten(), image.flatten(), (size[0], size[1]), eps=eps, isign=sign) if np.__name__ == 'cupy': result = np.asarray(result) return result
def generate_bandlimits(size=[512, 512], levels=9): """ A definition to calculate octaves used in bandlimiting frequencies in the frequency domain. Parameters ---------- size : list Size of each mask in octaves. Returns ---------- masks : ndarray Masks (Octaves). """ masks = np.zeros((levels, size[0], size[1])) cx = int(size[0] / 2) cy = int(size[1] / 2) for i in range(0, masks.shape[0]): deltax = int((size[0]) / (2**(i + 1))) deltay = int((size[1]) / (2**(i + 1))) masks[i, cx - deltax:cx + deltax, cy - deltay:cy + deltay] = 1. masks[i, int(cx - deltax / 2.):int(cx + deltax / 2.), int(cy - deltay / 2.):int(cy + deltay / 2.)] = 0. masks = np.asarray(masks) return masks
def intersect(self,ray): """ A definition to find out which surface to intersect a ray(s). Parameters ---------- ray : ndarray Ray(s) to be intersected. """ convex_normal,convex_distance = intersect_w_sphere( ray, self.convex_surface ) plane_normal,plane_distance = intersect_w_circle( ray, self.plane_surface ) test_normal = convex_normal if len(test_normal.shape) < 3: test_normal = convex_normal[0] is_it_in_lens = same_side( test_normal, self.convex_point, self.plane_surface[0][1], self.plane_surface[0][2] ) surface_normals = np.array([convex_normal,plane_normal],dtype=np.float) surface_distances = np.array([convex_distance,plane_distance],dtype=np.float) which_surface = np.amin(surface_distances,axis=0) ids = np.where(surface_distances==which_surface) ids = np.asarray(ids) # ids[0] = ids[0] | is_it_in_lens normal = surface_normals[ids[0],ids[1]] distance = surface_distances[ids[0],ids[1]] return normal,distance
def get_triangle_normal(triangle, triangle_center=None): """ Definition to calculate surface normal of a triangle. Parameters ---------- triangle : ndarray Set of points in X,Y and Z to define a planar surface (3,3). It can also be list of triangles (mx3x3). triangle_center : ndarray Center point of the given triangle. See odak.raytracing.center_of_triangle for more. In many scenarios you can accelerate things by precomputing triangle centers. Returns ---------- normal : ndarray Surface normal at the point of intersection. """ triangle = np.asarray(triangle) if len(triangle.shape) == 2: triangle = triangle.reshape((1, 3, 3)) normal = np.zeros((triangle.shape[0], 2, 3)) direction = np.cross(triangle[:, 0] - triangle[:, 1], triangle[:, 2] - triangle[:, 1]) if type(triangle_center) == type(None): normal[:, 0] = center_of_triangle(triangle) else: normal[:, 0] = triangle_center normal[:, 1] = direction / np.sum(direction, axis=1)[0] if normal.shape[0] == 1: normal = normal.reshape((2, 3)) return normal
def resize_image(img, target_size): if np.__name__ == 'cupy': import numpy img = np.asnumpy(img) img = scipy.misc.imresize(img, (target_size[0], target_size[1])) if np.__name__ == 'cupy': img = np.asarray(img) return img
def rotate_point(point,angles=[0,0,0],mode='XYZ',origin=[0,0,0],offset=[0,0,0]): """ Definition to rotate a given point. Note that rotation is always with respect to 0,0,0. Parameters ---------- point : ndarray A point. angles : list Rotation angles in degrees. mode : str Rotation mode determines ordering of the rotations at each axis. There are XYZ,YXZ,ZXY and ZYX modes. origin : list Reference point for a rotation. offset : list Shift with the given offset. Returns ---------- result : ndarray Result of the rotation rotx : ndarray Rotation matrix along X axis. roty : ndarray Rotation matrix along Y axis. rotz : ndarray Rotation matrix along Z axis. """ point = np.asarray(point) point -= np.asarray(origin) rotx = rotmatx(angles[0]) roty = rotmaty(angles[1]) rotz = rotmatz(angles[2]) if mode == 'XYZ': result = np.dot(rotz,np.dot(roty,np.dot(rotx,point))) elif mode == 'XZY': result = np.dot(roty,np.dot(rotz,np.dot(rotx,point))) elif mode == 'YXZ': result = np.dot(rotz,np.dot(rotx,np.dot(roty,point))) elif mode == 'ZXY': result = np.dot(roty,np.dot(rotx,np.dot(rotz,point))) elif mode == 'ZYX': result = np.dot(rotx,np.dot(roty,np.dot(rotz,point))) result += np.asarray(origin) result += np.asarray(offset) return result,rotx,roty,rotz
def write_PLY(triangles,savefn='output.ply'): """ Definition to generate a PLY file from given points. Parameters ---------- triangles : ndarray List of triangles with the size of Mx3x3. savefn : string Filename for a PLY file. """ tris = [] pnts = [] color = [255,255,255] for tri_id in range(triangles.shape[0]): tris.append( ( [3*tri_id,3*tri_id+1,3*tri_id+2], color[0], color[1], color[2] ) ) for i in range(0,3): pnts.append( ( float(triangles[tri_id][i][0]), float(triangles[tri_id][i][1]), float(triangles[tri_id][i][2]) ) ) if np.__name__ == 'cupy': import numpy as np_cpu tris = np_cpu.asarray(tris, dtype=[('vertex_indices', 'i4', (3,)),('red', 'u1'), ('green', 'u1'),('blue', 'u1')]) pnts = np_cpu.asarray(pnts, dtype=[('x', 'f4'), ('y', 'f4'), ('z', 'f4')]) else: tris = np.asarray(tris, dtype=[('vertex_indices', 'i4', (3,)),('red', 'u1'), ('green', 'u1'),('blue', 'u1')]) pnts = np.asarray(pnts, dtype=[('x', 'f4'), ('y', 'f4'), ('z', 'f4')]) # Save mesh. el1 = PlyElement.describe(pnts, 'vertex', comments=['Vertex data']) el2 = PlyElement.describe(tris, 'face', comments=['Face data']) PlyData([el1,el2],text="True").write(savefn)
def distance_between_two_points(point1, point2): """ Definition to calculate distance between two given points. Parameters ---------- point1 : list First point in X,Y,Z. point2 : list Second point in X,Y,Z. Returns ---------- distance : float Distance in between given two points. """ point1 = np.asarray(point1) point2 = np.asarray(point2) if len(point1.shape) == 1 and len(point2.shape) == 1: distance = np.sqrt(np.sum((point1 - point2)**2)) elif len(point1.shape) == 2 or len(point2.shape) == 2: distance = np.sqrt(np.sum((point1 - point2)**2, axis=1)) return distance
def create_ray_from_two_points(x0y0z0, x1y1z1): """ Definition to create a ray from two given points. Note that both inputs must match in shape. Parameters ---------- x0y0z0 : list List that contains X,Y and Z start locations of a ray (3). It can also be a list of points as well (mx3). This is the starting point. x1y1z1 : list List that contains X,Y and Z ending locations of a ray (3). It can also be a list of points as well (mx3). This is the end point. Returns ---------- ray : ndarray Array that contains starting points and cosines of a created ray. """ x0y0z0 = np.asarray(x0y0z0, dtype=np.float) x1y1z1 = np.asarray(x1y1z1, dtype=np.float) if len(x0y0z0.shape) == 1: x0y0z0 = x0y0z0.reshape((1, 3)) if len(x1y1z1.shape) == 1: x1y1z1 = x1y1z1.reshape((1, 3)) xdiff = x1y1z1[:, 0] - x0y0z0[:, 0] ydiff = x1y1z1[:, 1] - x0y0z0[:, 1] zdiff = x1y1z1[:, 2] - x0y0z0[:, 2] s = np.sqrt(xdiff**2 + ydiff**2 + zdiff**2) s[s == 0] = np.NaN cosines = np.zeros((xdiff.shape[0], 3)) cosines[:, 0] = xdiff / s cosines[:, 1] = ydiff / s cosines[:, 2] = zdiff / s ray = np.zeros((xdiff.shape[0], 2, 3), dtype=np.float) ray[:, 0] = x0y0z0 ray[:, 1] = cosines if ray.shape[0] == 1: ray = ray.reshape((2, 3)) return ray
def __init__(self,item='LA1024',location=[0.,0.,0.],rotation=[0.,0.,0.],wavelength=0.000532,meduium='air'): """ Class to represent plano-convex lens. Parameters ---------- item : str Plano convex lens label. Check Thorlabs catalog for labels. There are also json files within library. location : list Location in X,Y, and Z. rotation : list Rotation in X,Y, and Z. wavelength : float Wavelength in mm (default). medium : str Medium that the lens is in. Default is air. """ self.item = item self.location = np.asarray(location) self.rotation = np.asarray(rotation) self.path_to_catalog = "{}/data/plano_convex_lenses.json".format(os.path.dirname(odak.catalog.__file__)) self.settings = load_dictionary(self.path_to_catalog)[self.item] self.set_variables() self.define_geometry()
def roi(image, location=[0, 100, 0, 100], threshold=[0, 1, 0, 1]): """ Definition to get the lines from a target ROI. Parameters ---------- image : ndarray a 2D image to be sliced (nxm). location : ndarray Locations for taking the ROI. threshold : list Threshold below and above these numbers. Returns ------- line_x : ndarray Line slice. line_y : ndarray Line slice. """ img = image[location[0]:location[1], location[2]:location[3]] if len(img.shape) == 3: img = np.sum(img, axis=2) line_x = img[:, int(img.shape[1] / 2)] line_y = img[int(img.shape[0] / 2), :] line_x = np.asarray(line_x) line_y = np.asarray(line_y) line_x = line_x - np.amin(line_x) line_x = line_x / np.amax(line_x) line_y = line_y - np.amin(line_y) line_y = line_y / np.amax(line_y) line_x[line_x < threshold[0]] = 0 line_x[line_x > threshold[1]] = 1 line_y[line_y < threshold[2]] = 0 line_y[line_y > threshold[3]] = 1 return line_x, line_y, img
def intersect_parametric(ray, parametric_surface, surface_function, surface_normal_function, target_error=0.00000001, iter_no_limit=100000): """ Definition to intersect a ray with a parametric surface. Parameters ---------- ray : ndarray Ray. parametric_surface : ndarray Parameters of the surfaces. surface_function : function Function to evaluate a point against a surface. surface_normal_function : function Function to calculate surface normal for a given point on a surface. target_error : float Target error that defines the precision. iter_no_limit : int Maximum number of iterations. Returns ---------- distance : float Propagation distance. normal : ndarray Ray that defines a surface normal for the intersection. """ if len(ray.shape) == 2: ray = ray.reshape((1, 2, 3)) error = [150, 100] distance = [0, 0.1] iter_no = 0 while np.abs(np.max(np.asarray(error[1]))) > target_error: error[1], point = intersection_kernel_for_parametric_surfaces( distance[1], ray, parametric_surface, surface_function) distance, error = propagate_parametric_intersection_error( distance, error) iter_no += 1 if iter_no > iter_no_limit: return False, False if np.isnan(np.sum(point)): return False, False normal = surface_normal_function(point, parametric_surface) return distance[1], normal
def nuifft2(field, fx, fy, size=None, sign=1, eps=10**(-12)): """ A definition to take 2D Adjoint Non-Uniform Fast Fourier Transform (NUFFT). Parameters ---------- field : ndarray Input field. fx : ndarray Frequencies along x axis. fy : ndarray Frequencies along y axis. size : list or ndarray Shape of the NUFFT calculated for an input field. sign : float Sign of the exponential used in NUFFT kernel. eps : float Accuracy of NUFFT. Returns ---------- result : ndarray NUFFT of the input field. """ if np.__name__ == 'cupy': fx = np.asnumpy(fx).astype(np.float64) fy = np.asnumpy(fy).astype(np.float64) image = np.asnumpy(np.copy(field)).astype(np.complex128) else: image = np.copy(field).astype(np.complex128) if type(size) == type(None): result = finufft.nufft2d1(fx.flatten(), fy.flatten(), image.flatten(), image.shape, eps=eps, isign=sign) else: result = finufft.nufft2d1(fx.flatten(), fy.flatten(), image.flatten(), (size[0], size[1]), eps=eps, isign=sign) if np.__name__ == 'cupy': result = np.asarray(result) return result
def nuifft2(field, fx, fy, sign=1, eps=10**(-12)): """ """ if np.__name__ == 'cupy': fx = np.asnumpy(fx).astype(np.float64) fy = np.asnumpy(fy).astype(np.float64) image = np.asnumpy(np.copy(field)).astype(np.complex128) else: image = np.copy(field).astype(np.complex128) result = finufft.nufft2d2(fx.flatten(), fy.flatten(), image, eps=eps, isign=sign) result = result.reshape(field.shape) if np.__name__ == 'cupy': result = np.asarray(result) return result
def rotate_points(points,angles=[0,0,0],mode='XYZ',origin=[0,0,0],offset=[0,0,0]): """ Definition to rotate points. Parameters ---------- points : ndarray Points. angles : list Rotation angles in degrees. mode : str Rotation mode determines ordering of the rotations at each axis. There are XYZ,YXZ,ZXY and ZYX modes. origin : list Reference point for a rotation. offset : list Shift with the given offset. Returns ---------- result : ndarray Result of the rotation """ points = np.asarray(points) if angles[0] == 0 and angles[1] == 0 and angles[2] ==0: result = np.array(offset) + points return result points -= np.array(origin) rotx = rotmatx(angles[0]) roty = rotmaty(angles[1]) rotz = rotmatz(angles[2]) if mode == 'XYZ': result = np.dot(rotz,np.dot(roty,np.dot(rotx,points.T))).T elif mode == 'XZY': result = np.dot(roty,np.dot(rotz,np.dot(rotx,points.T))).T elif mode == 'YXZ': result = np.dot(rotz,np.dot(rotx,np.dot(roty,points.T))).T elif mode == 'ZXY': result = np.dot(roty,np.dot(rotx,np.dot(rotz,points.T))).T elif mode == 'ZYX': result = np.dot(rotx,np.dot(roty,np.dot(rotz,points.T))).T result += np.array(origin) result += np.array(offset) return result
def cross_product(vector1, vector2): """ Definition to cross product two vectors and return the resultant vector. Used method described under: http://en.wikipedia.org/wiki/Cross_product Parameters ---------- vector1 : ndarray A vector/ray. vector2 : ndarray A vector/ray. Returns ---------- ray : ndarray Array that contains starting points and cosines of a created ray. """ angle = np.cross(vector1[1].T, vector2[1].T) angle = np.asarray(angle) ray = np.array([vector1[0], angle], dtype=np.float) return ray
def read_PLY(fn,offset=[0,0,0],angles=[0.,0.,0.],mode='XYZ'): """ Definition to read a PLY file and extract meshes from a given PLY file. Note that rotation is always with respect to 0,0,0. Parameters ---------- fn : string Filename of a PLY file. offset : ndarray Offset in X,Y,Z. angles : list Rotation angles in degrees. mode : str Rotation mode determines ordering of the rotations at each axis. There are XYZ,YXZ,ZXY and ZYX modes. Returns ---------- triangles : ndarray Triangles from a given PLY file. Note that the triangles coming out of this function isn't always structured in the right order and with the size of (MxN)x3. You can use numpy's reshape to restructure it to mxnx3 if you know what you are doing. """ if np.__name__ != 'numpy': import numpy as np_ply else: np_ply = np with open(fn,'rb') as f: plydata = PlyData.read(f) triangle_ids = np_ply.vstack(plydata['face'].data['vertex_indices']) triangles = [] for vertex_ids in triangle_ids: triangle = [ rotate_point(plydata['vertex'][int(vertex_ids[0])].tolist(),angles=angles,offset=offset)[0], rotate_point(plydata['vertex'][int(vertex_ids[1])].tolist(),angles=angles,offset=offset)[0], rotate_point(plydata['vertex'][int(vertex_ids[2])].tolist(),angles=angles,offset=offset)[0] ] triangle = np_ply.asarray(triangle) triangles.append(triangle) triangles = np_ply.array(triangles) triangles = np.asarray(triangles,dtype=np.float) return triangles
def line_spread_function(line): """ Definition to take the gradient of a 1D function. Parameters ---------- line : ndarray 1D array. Returns ---------- result : ndarray Gradient of the given 1D array. """ if np.__name__ == 'cupy': import numpy cache = np.asnumpy(line) result = numpy.gradient(cache) else: result = np.gradient(line) result = np.asarray(result) return result
def intersect_w_surface(ray, points): """ Definition to find intersection point inbetween a surface and a ray. For more see: http://geomalgorithms.com/a06-_intersect-2.html Parameters ---------- ray : ndarray A vector/ray. points : ndarray Set of points in X,Y and Z to define a planar surface. Returns ---------- normal : ndarray Surface normal at the point of intersection. distance : float Distance in between starting point of a ray with it's intersection with a planar surface. """ points = np.asarray(points) normal = get_triangle_normal(points) if len(ray.shape) == 2: ray = ray.reshape((1, 2, 3)) if len(points) == 2: points = points.reshape((1, 3, 3)) if len(normal.shape) == 2: normal = normal.reshape((1, 2, 3)) f = normal[:, 0] - ray[:, 0] distance = np.dot(normal[:, 1], f.T) / np.dot(normal[:, 1], ray[:, 1].T) n = np.int(np.amax(np.array([ray.shape[0], normal.shape[0]]))) normal = np.zeros((n, 2, 3)) normal[:, 0] = ray[:, 0] + distance.T * ray[:, 1] distance = np.abs(distance) if normal.shape[0] == 1: normal = normal.reshape((2, 3)) distance = distance.reshape((1)) if distance.shape[0] == 1 and len(distance.shape) > 1: distance = distance.reshape((distance.shape[1])) return normal, distance
def raytrace(self, ray): """ Definition to raytrace the diffuser. Parameters ---------- ray : ndarray Ray(s). Returns ---------- new_rays : ndarray Diffusion rays. normal : ndarray Surface normals distance : ndarray Distance ray(s) has/have travelled until to the point of diffusion. """ if len(ray.shape) == 2: ray = ray.reshape((1, ray.shape[0], ray.shape[1])) normal, distance = intersect_w_surface(ray, self.plane) ######################################################################## # This is the bottleneck for has to be replaced for better performance # ######################################################################## new_rays = np.empty((0, 2, 3)) for point_id in range(0, normal.shape[0]): tilt_angles = tilt_towards(ray[point_id, 0], normal[point_id, 0]) tilted_points = rotate_points(self.diffusion_points, angles=tilt_angles, offset=normal[point_id, 0]) new_rays = np.vstack( (new_rays, create_ray_from_two_points(normal[point_id, 0], tilted_points))) new_rays = np.asarray(new_rays) return new_rays, normal, distance
def point_wise(field,distances,k,dx,wavelength,lens_method='ideal',propagation_method='Bandlimited Angular Spectrum',n_iteration=3): """ Point-wise hologram calculation method. For more Maimone, Andrew, Andreas Georgiou, and Joel S. Kollin. "Holographic near-eye displays for virtual and augmented reality." ACM Transactions on Graphics (TOG) 36.4 (2017): 1-16. Parameters ---------- field : ndarray Complex input field to be converted into a hologram. distances : ndarray Depth map of the input field. k : odak.wave.wavenumber Wave number of a wave, see odak.wave.wavenumber for more. dx : float Pixel pitch. wavelength : float Wavelength of the light. lens_model : str Method to calculate the lens patterns. propagation_mode : str Beam propagation method to be used if the lens_model is not equal to `ideal`. n_iteration : int Number of iterations. Returns ---------- hologram : ndarray Generated complex hologram. """ hologram = np.zeros(field.shape,dtype=np.complex64) nx,ny = field.shape cx = int(nx/2) cy = int(ny/2) non_zeros = np.asarray((np.abs(field)>0).nonzero()) unique_dist = np.unique(distances) unique_dist = unique_dist[unique_dist!=0] target = np.zeros((nx,ny),dtype=np.complex64) target[cx,cy] = 1. lenses = [] for distance in unique_dist: if lens_method == 'ideal': new_lens = quadratic_phase_function(nx,ny,k,focal=distance,dx=dx) lenses.append(new_lens) elif lens_method == 'Gerchberg-Saxton': new_lens,_ = gerchberg_saxton( target, n_iteration, distance, dx, wavelength, np.pi*2, propagation_method ) lenses.append(new_lens) for m in tqdm(range(non_zeros.shape[1])): i = int(non_zeros[0,m]) j = int(non_zeros[1,m]) lens_id = int(np.argwhere(unique_dist==distances[i,j])) lens = lenses[lens_id] lens = np.roll(lens,i-cx,axis=0) lens = np.roll(lens,j-cy,axis=1) hologram += lens*field[i,j] return hologram