Coverage for kwave/utils/maputils.py: 8%
1021 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-24 11:55 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-24 11:55 -0700
1import math
2from math import floor
3from typing import Tuple, Optional
5import matplotlib.pyplot as plt
6import numpy as np
7from kwave.utils.tictoc import TicToc
9from kwave.utils.matrixutils import matlab_find, matlab_assign, max_nd
10from kwave.utils.conversionutils import scale_SI
11from scipy import optimize
12import warnings
14from kwave.utils.conversionutils import db2neper, neper2db
17def get_spaced_points(start, stop, n=100, spacing='linear'):
18 """
19 getSpacedPoints generates a row vector of either logarithmically or
20 linearly spaced points between X1 and X2. When spacing is set to
21 'linear', the function is identical to the inbuilt np.linspace
22 function. When spacing is set to 'log', the function is similar to
23 the inbuilt np.logspace function, except that X1 and X2 define the start
24 and end numbers, not decades. For logarithmically spaced points, X1
25 must be > 0. If N < 2, X2 is returned.
26 Args:
27 start:
28 stop:
29 n:
30 spacing:
32 Returns:
33 points:
34 """
35 # check if the end point is larger than the start point
36 if stop <= start:
37 raise ValueError('X2 must be larger than X1.')
39 if spacing == 'linear':
40 return np.linspace(start, stop, num=n)
41 elif spacing == 'log':
42 return np.geomspace(start, stop, num=n)
43 else:
44 raise ValueError(f"spacing {spacing} is not a valid argument. Choose from 'linear' or 'log'.")
47def pull_matlab(prop, expected_val=None):
48 from scipy.io import loadmat
50 temp_mat = loadmat('/data/code/Work/black_box_testing/temp.mat')
51 if expected_val is not None:
52 return np.allclose(expected_val, temp_mat[prop])
53 return temp_mat[prop]
56def fit_power_law_params(a0, y, c0, f_min, f_max, plot_fit=False):
57 """
59 fit_power_law_params calculates the absorption parameters that should
60 be defined in the simulation functions given the desired power law
61 absorption behaviour defined by a0 and y. This takes into account the
62 actual absorption behaviour exhibited by the fractional Laplacian
63 wave equation.
65 This fitting is required when using large absorption values or high
66 frequencies, as the fractional Laplacian wave equation solved in
67 kspaceFirstOrderND and kspaceSecondOrder no longer encapsulates
68 absorption of the form a = a0*f^y.
70 The returned values should be used to define the medium.alpha_coeff
71 and medium.alpha_power within the simulation functions. The
72 absorption behaviour over the frequency range f_min:f_max will then
73 follow the power law defined by a0 and y.Add testing for getOptimalPMLSize()
75 Args:
76 a0:
77 y:
78 c0:
79 f_min:
80 f_max:
81 plot_fit:
83 Returns:
84 a0_fit:
85 y_fit:
87 """
88 # define frequency axis
89 f = get_spaced_points(f_min, f_max, 200)
90 w = 2 * np.pi * f
91 # convert user defined a0 to Nepers/((rad/s)^y m)
92 a0_np = db2neper(a0, y)
94 desired_absorption = a0_np * w ** y
96 def abs_func(trial_vals):
97 """Second-order absorption error"""
98 a0_np_trial, y_trial = trial_vals
100 actual_absorption = a0_np_trial * w ** y_trial / (1 - (y_trial + 1) * \
101 a0_np_trial * c0 * np.tan(np.pi * y_trial / 2) * w ** (
102 y_trial - 1))
104 absorption_error = np.sqrt(np.sum((desired_absorption - actual_absorption) ** 2))
106 return absorption_error
108 a0_np_fit, y_fit = optimize.fmin(abs_func, [a0_np, y])
110 a0_fit = neper2db(a0_np_fit, y_fit)
112 if plot_fit:
113 raise NotImplementedError
115 return a0_fit, y_fit
118def power_law_kramers_kronig(w, w0, c0, a0, y):
119 """
120 POWERLAWKRAMERSKRONIG Calculate dispersion for power law absorption.
122 DESCRIPTION:
123 powerLawKramersKronig computes the variation in sound speed for an
124 attenuating medium using the Kramers-Kronig for power law
125 attenuation where att = a0*w^y. The power law parameters must be in
126 Nepers / m, with the frequency in rad/s. The variation is given
127 about the sound speed c0 at a reference frequency w0.
129 USAGE:
130 c_kk = power_law_kramers_kronig(w, w0, c0, a0, y)
132 INPUTS:
134 OUTPUTS:
136 Args:
137 w: input frequency array [rad/s]
138 w0: reference frequency [rad/s]
139 c0: sound speed at w0 [m/s]
140 a0: power law coefficient [Nepers/((rad/s)^y m)]
141 y: power law exponent, where 0 < y < 3
143 Returns:
144 c_kk variation of sound speed with w [m/s]
146 """
147 if 0 >= y or y >= 3:
148 warnings.warn("y must be within the interval (0,3)", UserWarning)
149 c_kk = c0 * np.ones_like(w)
150 elif y == 1:
151 # Kramers-Kronig for y = 1
152 c_kk = 1 / (1 / c0 - 2 * a0 * np.log(w / w0) / np.pi)
153 else:
154 # Kramers-Kronig for 0 < y < 1 and 1 < y < 3
155 c_kk = 1 / (1 / c0 + a0 * np.tan(y * np.pi / 2) * (w ** (y - 1) - w0 ** (y - 1)))
157 return c_kk
160def water_absorption(f, temp):
161 """
162 water_absorption Calculate ultrasound absorption in distilled water.
164 DESCRIPTION:
165 waterAbsorption calculates the ultrasonic absorption in distilled
166 water at a given temperature and frequency using a 7 th order
167 polynomial fitted to the data given by np.pinkerton(1949).
169 USAGE:
170 abs = waterAbsorption(f, T)
172 INPUTS:
173 f - f frequency value [MHz]
174 T - water temperature value [degC]
176 OUTPUTS:
177 abs - absorption[dB / cm]
179 REFERENCES:
180 [1] np.pinkerton(1949) "The Absorption of Ultrasonic Waves in Liquids
181 and its Relation to Molecular Constitution, " Proceedings of the
182 Physical Society.Section B, 2, 129 - 141
184 """
186 NEPER2DB = 8.686
187 # check temperature is within range
188 if not 0 <= temp <= 60:
189 raise Warning("Temperature outside range of experimental data")
191 # conversion factor between Nepers and dB NEPER2DB = 8.686;
192 # coefficients for 7th order polynomial fit
193 a = [56.723531840522710, -2.899633796917384, 0.099253401567561, -0.002067402501557, 2.189417428917596e-005,
194 -6.210860973978427e-008, -6.402634551821596e-010, 3.869387679459408e-012]
196 # TODO: this is not a vectorized version of this function. This is different than the MATLAB version
197 # make sure vectors are in the correct orientation
198 # T = reshape(T, 3, []);
199 # f = reshape(f, [], 1);
201 # compute absorption
202 a_on_fsqr = (a[0] + a[1] * temp + a[2] * temp ** 2 + a[3] * temp ** 3 + a[4] * temp ** 4 + a[5] * temp ** 5 + a[
203 6] * temp ** 6 + a[7] * temp ** 7) * 1e-17
205 abs = NEPER2DB * 1e12 * f ** 2 * a_on_fsqr
206 return abs
209def hounsfield2soundspeed(ct_data):
210 """
212 Calclulates the soundspeed of a medium given a CT of the medium.
213 For soft-tissue, the approximate sound speed can also be returned
214 using the empirical relationship given by Mast.
216 Mast, T. D., "Empirical relationships between acoustic parameters
217 in human soft tissues," Acoust. Res. Lett. Online, 1(2), pp. 37-42
218 (2000).
220 Args:
221 ct_data:
223 Returns: sound_speed: matrix of sound speed values of size of ct data
225 """
226 # calculate corresponding sound speed values if required using soft tissue relationship
227 # TODO confirm that this linear relationship is correct
228 sound_speed = (hounsfield2density(ct_data) + 349) / 0.893
230 return sound_speed
233def hounsfield2density(ct_data, plot_fitting=False):
234 """
235 Convert Hounsfield units in CT data to density values [kg / m ^ 3]
236 based on the experimental data given by Schneider et al.
237 The conversion is made using a np.piece-wise linear fit to the data.
239 Args:
240 ct_data:
241 plot_fitting:
243 Returns:
244 density_map: density in kg / m ^ 3
245 """
246 # create empty density matrix
247 density = np.zeros(ct_data.shape, like=ct_data)
249 # apply conversion in several parts using linear fits to the data
250 # Part 1: Less than 930 Hounsfield Units
251 density[ct_data < 930] = np.polyval([1.025793065681423, -5.680404011488714], ct_data[ct_data < 930])
253 # Part 2: Between 930 and 1098(soft tissue region)
254 index_selection = np.logical_and(930 <= ct_data, ct_data <= 1098)
255 density[index_selection] = np.polyval([0.9082709691264, 103.6151457847139],
256 ct_data[index_selection])
258 # Part 3: Between 1098 and 1260(between soft tissue and bone)
259 index_selection = np.logical_and(1098 < ct_data, ct_data < 1260)
260 density[index_selection] = np.polyval([0.5108369316599, 539.9977189228704], ct_data[index_selection])
262 # Part 4: Greater than 1260(bone region)
263 density[ct_data >= 1260] = np.polyval([0.6625370912451, 348.8555178455294], ct_data[ct_data >= 1260])
265 if plot_fitting:
266 raise NotImplementedError("Plotting function not implemented in Python")
268 return density
271def water_sound_speed(temp):
272 """
273 WATERSOUNDSPEED Calculate the sound speed in distilled water with temperature.
275 DESCRIPTION:
276 waterSoundSpeed calculates the sound speed in distilled water at a
277 a given temperature using the 5th order polynomial given by Marczak
278 (1997).
280 USAGE:
281 c = waterSoundSpeed(T)
283 INPUTS:
284 T - water temperature in the range 0 to 95 [degC]
286 OUTPUTS:
287 c - sound speed [m/s]
289 REFERENCES:
290 [1] Marczak (1997) "Water as a standard in the measurements of speed
291 of sound in liquids," J. Acoust. Soc. Am., 102, 2776-2779.
293 """
295 # check limits
296 assert 95 >= temp >= 0, "temp must be between 0 and 95."
298 # find value
299 p = [2.787860e-9, -1.398845e-6, 3.287156e-4, -5.779136e-2, 5.038813, 1.402385e3]
300 c = np.polyval(p, temp)
301 return c
304def water_density(temp):
305 """
306 WATERDENSITY Calculate density of air - saturated water with temperature.
308 DESCRIPTION:
309 waterDensity calculates the density of air - saturated water at a given % temperature using the 4 th order polynomial given by Jones(1992).
311 USAGE:
312 density = waterDensity(T)
314 INPUTS:
315 T - water temperature in the range 5 to 40[degC]
317 OUTPUTS:
318 density - density of water[kg / m ^ 3]
320 ABOUT:
321 author - Bradley E.Treeby
322 date - 22 nd February 2018
323 last update - 4 th April 2019
325 REFERENCES:
326 [1] F.E.Jones and G.L.Harris(1992) "ITS-90 Density of Water
327 Formulation for Volumetric Standards Calibration, " J. Res. Natl.
328 Inst.Stand.Technol., 97(3), 335 - 340.
329 """
330 # check limits
331 assert 5 <= temp <= 40, "T must be between 5 and 40."
333 # calculate density of air - saturated water
334 density = 999.84847 + 6.337563e-2 * temp - 8.523829e-3 * temp ** 2 + 6.943248e-5 * temp ** 3 - 3.821216e-7 * temp ** 4
335 return density
338def water_non_linearity(temp):
339 """
340 WATERNONLINEARITY Calculate B/A of water with temperature.
342 DESCRIPTION:
343 waterNonlinearity calculates the parameter of nonlinearity B/A at a
344 given temperature using a fourth-order polynomial fitted to the data
345 given by Beyer (1960).
347 USAGE:
348 BonA = waterNonlinearity(T)
350 INPUTS:
351 T - water temperature in the range 0 to 100 [degC]
353 OUTPUTS:
354 BonA - parameter of nonlinearity
356 ABOUT:
357 author - Bradley E. Treeby
358 date - 22nd February 2018
359 last update - 4th April 2019
361 REFERENCES:
362 [1] R. T Beyer (1960) "Parameter of nonlinearity in fluids," J.
363 Acoust. Soc. Am., 32(6), 719-721.
365 """
367 # check limits
368 assert 0 <= temp <= 100, "Temp must be between 0 and 100."
370 # find value
371 p = [-4.587913769504693e-08, 1.047843302423604e-05, -9.355518377254833e-04, 5.380874771364909e-2, 4.186533937275504]
372 BonA = np.polyval(p, temp);
373 return BonA
376def makeBall(Nx, Ny, Nz, cx, cy, cz, radius, plot_ball=False, binary=False):
377 """
378 MAKEBALL Create a binary map of a filled ball within a 3D grid.
380 DESCRIPTION:
381 makeBall creates a binary map of a filled ball within a
382 three-dimensional grid (the ball position is denoted by 1's in the
383 matrix with 0's elsewhere). A single grid point is taken as the ball
384 centre thus the total diameter of the ball will always be an odd
385 number of grid points.
386 Args:
387 Nx: size of the 3D grid in x-dimension [grid points]
388 Ny: size of the 3D grid in y-dimension [grid points]
389 Nz: size of the 3D grid in z-dimension [grid points]
390 cx: centre of the ball in x-dimension [grid points]
391 cy: centre of the ball in y-dimension [grid points]
392 cz: centre of the ball in z-dimension [grid points]
393 radius: ball radius [grid points]
394 plot_ball: Boolean controlling whether the ball is plotted using voxelPlot (default = false)
395 binary: Boolean controlling whether the ball map is returned as a double precision matrix (false)
396 or a logical matrix (true) (default = false)
398 Returns:
399 3D binary map of a filled ball
400 """
401 # define literals
402 MAGNITUDE = 1
404 # force integer values
405 Nx = int(round(Nx))
406 Ny = int(round(Ny))
407 Nz = int(round(Nz))
408 cx = int(round(cx))
409 cy = int(round(cy))
410 cz = int(round(cz))
412 # check for zero values
413 if cx == 0:
414 cx = int(floor(Nx / 2)) + 1
416 if cy == 0:
417 cy = int(floor(Ny / 2)) + 1
419 if cz == 0:
420 cz = int(floor(Nz / 2)) + 1
422 # create empty matrix
423 ball = np.zeros((Nx, Ny, Nz)).astype(np.bool if binary else np.float32)
425 # define np.pixel map
426 r = makePixelMap(Nx, Ny, Nz, 'Shift', [0, 0, 0])
428 # create ball
429 ball[r <= radius] = MAGNITUDE
431 # shift centre
432 cx = cx - int(math.ceil(Nx / 2))
433 cy = cy - int(math.ceil(Ny / 2))
434 cz = cz - int(math.ceil(Nz / 2))
435 ball = np.roll(ball, (cx, cy, cz), axis=(0, 1, 2))
437 # plot results
438 if plot_ball:
439 raise NotImplementedError
440 # voxelPlot(double(ball))
441 return ball
444def make_cart_sphere(radius, num_points, center_pos=(0, 0, 0), plot_sphere=False):
445 """
446 make_cart_sphere creates a set of points in cartesian coordinates defining a sphere.
448 Args:
449 radius:
450 num_points:
451 center_pos:
452 plot_sphere:
454 Returns:
456 """
457 cx, cy, cz = center_pos
459 # generate angle functions using the Golden Section Spiral method
460 inc = np.pi * (3 - np.sqrt(5))
461 off = 2 / num_points
462 k = np.arange(0, num_points)
463 y = k * off - 1 + (off / 2)
464 r = np.sqrt(1 - (y ** 2))
465 phi = k * inc
467 # create the sphere
468 sphere = radius * np.concatenate([np.cos(phi) * r[np.newaxis, :], y[np.newaxis, :], np.sin(phi) * r[np.newaxis, :]])
470 # offset if needed
471 sphere[0, :] = sphere[0, :] + cx
472 sphere[1, :] = sphere[1, :] + cy
473 sphere[2, :] = sphere[2, :] + cz
475 # plot results
476 if plot_sphere:
477 # select suitable axis scaling factor
478 [x_sc, scale, prefix, _] = scale_SI(np.max(sphere))
480 # create the figure
481 plt.figure
482 plt.style.use('seaborn-poster')
483 ax = plt.axes(projection='3d')
484 ax.plot3D(sphere[0, :] * scale, sphere[1, :] * scale, sphere[2, :] * scale, '.')
485 ax.set_xlabel(f"[{prefix} m]")
486 ax.set_ylabel(f"[{prefix} m]")
487 ax.set_zlabel(f"[{prefix} m]")
488 ax.axis('auto')
489 ax.grid()
490 plt.show()
493def make_cart_circle(radius, num_points, center_pos=(0, 0), arc_angle=2 * np.pi, plot_circle=False):
494 """
495 make_cart_circle creates a set of points in cartesian coordinates defining a circle or arc.
497 Args:
498 radius:
499 num_points:
500 center_pos:
501 arc_angle: Arc angle in radians.
502 plot_circle:
504 Returns:
505 2 x num_points array of cartesian coordinates
507 """
509 # check for arc_angle input
510 if arc_angle == 2 * np.pi:
511 full_circle = True
513 cx = center_pos[0]
514 cy = center_pos[1]
516 # create angles
517 angles = np.arange(0, num_points) * arc_angle / num_points + np.pi / 2
519 # create cartesian grid
520 circle = np.concatenate([radius * np.cos(angles[np.newaxis, :]), radius * np.sin(-angles[np.newaxis])])
522 # offset if needed
523 circle[0, :] = circle[0, :] + cx
524 circle[1, :] = circle[1, :] + cy
526 # plot results
527 if plot_circle:
528 # select suitable axis scaling factor
529 [_, scale, prefix, _] = scale_SI(np.max(abs(circle)))
531 # create the figure
532 plt.figure()
533 plt.plot(circle[1, :] * scale, circle[0, :] * scale, 'b.')
534 plt.xlabel([f"y-position [{prefix} m]"])
535 plt.ylabel([f"x-position [{prefix} m]"])
536 plt.axis('equal')
537 plt.show()
540def makeDisc(Nx, Ny, cx, cy, radius, plot_disc=False):
541 """
542 Create a binary map of a filled disc within a 2D grid.
544 makeDisc creates a binary map of a filled disc within a
545 two-dimensional grid (the disc position is denoted by 1's in the
546 matrix with 0's elsewhere). A single grid point is taken as the disc
547 centre thus the total diameter of the disc will always be an odd
548 number of grid points. As the returned disc has a constant radius, if
549 used within a k-Wave grid where dx ~= dy, the disc will appear oval
550 shaped. If part of the disc overlaps the grid edge, the rest of the
551 disc will wrap to the grid edge on the opposite side.
552 Args:
553 Nx:
554 Ny:
555 cx:
556 cy:
557 radius:
558 plot_disc:
560 Returns:
562 """
563 # define literals
564 MAGNITUDE = 1
566 # force integer values
567 Nx = int(round(Nx))
568 Ny = int(round(Ny))
569 cx = int(round(cx))
570 cy = int(round(cy))
572 # check for zero values
573 if cx == 0:
574 cx = int(floor(Nx / 2)) + 1
576 if cy == 0:
577 cy = int(floor(Ny / 2)) + 1
579 # check the inputs
580 assert (0 <= cx < Nx) and (0 <= cy < Ny), 'Disc center must be within grid.'
582 # create empty matrix
583 disc = np.zeros((Nx, Ny))
585 # define np.pixel map
586 r = makePixelMap(Nx, Ny, None, 'Shift', [0, 0])
588 # create disc
589 disc[r <= radius] = MAGNITUDE
591 # shift centre
592 cx = cx - int(math.ceil(Nx / 2))
593 cy = cy - int(math.ceil(Ny / 2))
594 disc = np.roll(disc, (cx, cy), axis=(0, 1))
596 # create the figure
597 if plot_disc:
598 raise NotImplementedError
599 return disc
602def makeCircle(Nx, Ny, cx, cy, radius, arc_angle=None, plot_circle=False):
603 """
604 Create a binary map of a circle within a 2D grid.
606 makeCircle creates a binary map of a circle or arc (using the
607 midpoint circle algorithm) within a two-dimensional grid (the circle
608 position is denoted by 1's in the matrix with 0's elsewhere). A
609 single grid point is taken as the circle centre thus the total
610 diameter will always be an odd number of grid points.
612 Note: The centre of the circle and the radius are not constrained by
613 the grid dimensions, so it is possible to create sections of circles,
614 or a blank image if none of the circle intersects the grid.
615 Args:
616 Nx:
617 Ny:
618 cx:
619 cy:
620 radius:
621 plot_disc:
623 Returns:
625 """
626 # define literals
627 MAGNITUDE = 1
629 if arc_angle is None:
630 arc_angle = 2 * np.pi
631 elif arc_angle > 2 * np.pi:
632 arc_angle = 2 * np.pi
633 elif arc_angle < 0:
634 arc_angle = 0
636 # force integer values
637 Nx = int(round(Nx))
638 Ny = int(round(Ny))
639 cx = int(round(cx))
640 cy = int(round(cy))
641 radius = int(round(radius))
643 # check for zero values
644 if cx == 0:
645 cx = int(floor(Nx / 2)) + 1
647 if cy == 0:
648 cy = int(floor(Ny / 2)) + 1
650 # create empty matrix
651 circle = np.zeros((Nx, Ny), dtype=int)
653 # initialise loop variables
654 x = 0
655 y = radius
656 d = 1 - radius
658 if (cx >= 1) and (cx <= Nx) and ((cy - y) >= 1) and ((cy - y) <= Ny):
659 circle[cx - 1, cy - y - 1] = MAGNITUDE
661 # draw the remaining cardinal points
662 px = [cx, cx + y, cx - y]
663 py = [cy + y, cy, cy]
664 for point_index, (px_i, py_i) in enumerate(zip(px, py)):
665 # check whether the point is within the arc made by arc_angle, and lies
666 # within the grid
667 if (np.arctan2(px_i - cx, py_i - cy) + np.pi) <= arc_angle:
668 if (px_i >= 1) and (px_i <= Nx) and (py_i >= 1) and (
669 py_i <= Ny):
670 circle[px_i - 1, py_i - 1] = MAGNITUDE
672 # loop through the remaining points using the midpoint circle algorithm
673 while x < (y - 1):
675 x = x + 1
676 if d < 0:
677 d = d + x + x + 1
678 else:
679 y = y - 1
680 a = x - y + 1
681 d = d + a + a
683 # setup point indices (break coding standard for readability)
684 px = [x + cx, y + cx, y + cx, x + cx, -x + cx, -y + cx, -y + cx, -x + cx]
685 py = [y + cy, x + cy, -x + cy, -y + cy, -y + cy, -x + cy, x + cy, y + cy]
687 # loop through each point
688 for point_index, (px_i, py_i) in enumerate(zip(px, py)):
690 # check whether the point is within the arc made by arc_angle, and
691 # lies within the grid
692 if (np.arctan2(px_i - cx, py_i - cy) + np.pi) <= arc_angle:
693 if (px_i >= 1) and (px_i <= Nx) and (py_i >= 1) and (py_i <= Ny):
694 circle[px_i - 1, py_i - 1] = MAGNITUDE
696 if plot_circle:
697 plt.imshow(circle, cmap='gray_r')
698 plt.ylabel('x-position [grid points]')
699 plt.xlabel('y-position [grid points]')
700 plt.show()
702 return circle
705def makeCartCircle(radius, num_points, center_pos=None, arc_angle=(2 * np.pi), plot_circle=False):
706 """
707 Create a 2D Cartesian circle or arc.
709 MakeCartCircle creates a 2 x num_points array of the Cartesian
710 coordinates of points evenly distributed over a circle or arc (if
711 arc_angle is given).
712 Args:
714 Returns:
716 """
717 full_circle = (arc_angle == 2 * np.pi)
719 if center_pos is None:
720 cx = cy = 0
721 else:
722 cx, cy = center_pos
724 # ensure there is only a total of num_points including the endpoints when
725 # arc_angle is not equal to 2*pi
726 if not full_circle:
727 num_points = num_points - 1
729 # create angles
730 angles = np.arange(0, num_points + 1) * arc_angle / num_points + np.pi / 2
732 # discard repeated final point if arc_angle is equal to 2*pi
733 if full_circle:
734 angles = angles[0:- 1]
736 # create cartesian grid
737 # circle = flipud([radius*cos(angles); radius*sin(-angles)]); # B.0.3
738 circle = np.vstack([radius * np.cos(angles), radius * np.sin(-angles)]) # B.0.4
740 # offset if needed
741 circle[0, :] = circle[0, :] + cx
742 circle[1, :] = circle[1, :] + cy
744 if plot_circle:
745 raise NotImplementedError
747 return circle
750def makePixelMap(Nx, Ny, Nz=None, *args):
751 """
752 MAKEPIXELMAP Create matrix of grid point distances from the centre point.
754 DESCRIPTION:
755 makePixelMap generates a matrix populated with values of how far each
756 pixel in a grid is from the centre (given in pixel coordinates). Both
757 single and double pixel centres can be used by setting the optional
758 input parameter 'OriginSize'. For grids where the dimension size and
759 centre pixel size are not both odd or even, the optional input
760 parameter 'Shift' can be used to control the location of the
761 centerpoint.
763 examples for a 2D pixel map:
765 Single pixel origin size for odd and even (with 'Shift' = [1 1] and
766 [0 0], respectively) grid sizes:
768 x x x x x x x x x x x
769 x 0 x x x x x x 0 x x
770 x x x x x 0 x x x x x
771 x x x x x x x x
773 Double pixel origin size for even and odd (with 'Shift' = [1 1] and
774 [0 0], respectively) grid sizes:
776 x x x x x x x x x x x x x x
777 x 0 0 x x x x x x x 0 0 x x
778 x 0 0 x x x 0 0 x x 0 0 x x
779 x x x x x x 0 0 x x x x x x
780 x x x x x x x x x x
782 By default a single pixel centre is used which is shifted towards
783 the final row and column.
784 Args:
785 *args:
787 Returns:
789 """
790 # define defaults
791 origin_size = 'single'
792 shift_def = 1
794 # detect whether the inputs are for two or three dimensions
795 if Nz is None:
796 map_dimension = 2
797 shift = [shift_def, shift_def]
798 else:
799 map_dimension = 3
800 shift = [shift_def, shift_def, shift_def]
802 # replace with user defined values if provided
803 if len(args) > 0:
804 assert len(args) % 2 == 0, 'Optional inputs must be entered as param, value pairs.'
805 for input_index in range(0, len(args), 2):
806 if args[input_index] == 'Shift':
807 shift = args[input_index + 1]
808 elif args[input_index] == 'OriginSize':
809 origin_size = args[input_index + 1]
810 else:
811 raise ValueError('Unknown optional input.')
813 # catch input errors
814 assert origin_size in ['single', 'double'], 'Unknown setting for optional input Center.'
816 assert len(
817 shift) == map_dimension, f'Optional input Shift must have {map_dimension} elements for {map_dimension} dimensional input parameters.'
819 if map_dimension == 2:
820 # create the maps for each dimension
821 nx = createPixelDim(Nx, origin_size, shift[0])
822 ny = createPixelDim(Ny, origin_size, shift[1])
824 # create plaid grids
825 r_x, r_y = np.meshgrid(nx, ny, indexing='ij')
827 # extract the pixel radius
828 r = np.sqrt(r_x ** 2 + r_y ** 2)
829 if map_dimension == 3:
830 # create the maps for each dimension
831 nx = createPixelDim(Nx, origin_size, shift[0])
832 ny = createPixelDim(Ny, origin_size, shift[1])
833 nz = createPixelDim(Nz, origin_size, shift[2])
835 # create plaid grids
836 r_x, r_y, r_z = np.meshgrid(nx, ny, nz, indexing='ij')
838 # extract the pixel radius
839 r = np.sqrt(r_x ** 2 + r_y ** 2 + r_z ** 2)
840 return r
843def createPixelDim(Nx, origin_size, shift):
844 # Nested function to create the pixel radius variable
846 # grid dimension has an even number of points
847 if Nx % 2 == 0:
849 # pixel numbering has a single centre point
850 if origin_size == 'single':
852 # centre point is shifted towards the final pixel
853 if shift == 1:
854 nx = np.arange(-Nx / 2, Nx / 2 - 1 + 1, 1)
856 # centre point is shifted towards the first pixel
857 else:
858 nx = np.arange(-Nx / 2 + 1, Nx / 2 + 1, 1)
860 # pixel numbering has a double centre point
861 else:
862 nx = np.hstack([np.arange(-Nx / 2 + 1, 0 + 1, 1), np.arange(0, -Nx / 2 - 1 + 1, 1)])
864 # grid dimension has an odd number of points
865 else:
867 # pixel numbering has a single centre point
868 if origin_size == 'single':
869 nx = np.arange(-(Nx - 1) / 2, (Nx - 1) / 2 + 1, 1)
871 # pixel numbering has a double centre point
872 else:
874 # centre point is shifted towards the final pixel
875 if shift == 1:
876 nx = np.hstack([np.arange(-(Nx - 1) / 2, 0 + 1, 1), np.arange(0, (Nx - 1) / 2 - 1 + 1, 1)])
878 # centre point is shifted towards the first pixel
879 else:
880 nx = np.hstack([np.arange(-(Nx - 1) / 2 + 1, 0 + 1, 1), np.arange(0, (Nx - 1) / 2 + 1, 1)])
881 return nx
884def makeLine(
885 Nx: int,
886 Ny: int,
887 startpoint: Tuple[int, int],
888 endpoint: Optional[Tuple[int, int]] = None,
889 angle: Optional[float] = None,
890 length: Optional[int] = None
891) -> np.ndarray:
892 # =========================================================================
893 # INPUT CHECKING
894 # =========================================================================
896 startpoint = np.array(startpoint, dtype=int)
897 if endpoint is not None:
898 endpoint = np.array(endpoint, dtype=int)
900 if len(startpoint) != 2:
901 raise ValueError('startpoint should be a two-element vector.')
903 if np.any(startpoint < 1) or startpoint[0] > Nx or startpoint[1] > Ny:
904 ValueError('The starting point must lie within the grid, between [1 1] and [Nx Ny].')
906 # =========================================================================
907 # LINE BETWEEN TWO POINTS OR ANGLED LINE?
908 # =========================================================================
910 if endpoint is not None:
911 linetype = 'AtoB'
912 a, b = startpoint, endpoint
914 # Addition => Fix Matlab2Python indexing
915 a -= 1
916 b -= 1
917 else:
918 linetype = 'angled'
919 angle, linelength = angle, length
921 # =========================================================================
922 # MORE INPUT CHECKING
923 # =========================================================================
925 if linetype == 'AtoB':
927 # a and b must be different points
928 if np.all(a == b):
929 raise ValueError('The first and last points cannot be the same.')
931 # end point must be a two-element row vector
932 if len(b) != 2:
933 raise ValueError('endpoint should be a two-element vector.')
935 # a and b must be within the grid
936 xx = np.array([a[0], b[0]], dtype=int)
937 yy = np.array([a[1], b[1]], dtype=int)
938 if np.any(a < 0) or np.any(b < 0) or np.any(xx > Nx - 1) or np.any(yy > Ny - 1):
939 raise ValueError('Both the start and end points must lie within the grid.')
941 if linetype == 'angled':
943 # angle must lie between -np.pi and np.pi
944 angle = angle % (2 * np.pi)
945 if angle > np.pi:
946 angle = angle - (2 * np.pi)
947 elif angle < -np.pi:
948 angle = angle + (2 * np.pi)
950 # =========================================================================
951 # CALCULATE A LINE FROM A TO B
952 # =========================================================================
954 if linetype == 'AtoB':
956 # define an empty grid to hold the line
957 line = np.zeros((Nx, Ny))
959 # find the equation of the line
960 m = (b[1] - a[1]) / (b[0] - a[0]) # gradient of the line
961 c = a[1] - m * a[0] # where the line crosses the y axis
963 if abs(m) < 1:
965 # start at the end with the smallest value of x
966 if a[0] < b[0]:
967 x, y = a
968 x_end = b[0]
969 else:
970 x, y = b
971 x_end = a[0]
973 # fill in the first point
974 line[x, y] = 1
976 while x < x_end:
977 # next points to try are
978 poss_x = [x, x, x + 1, x + 1, x + 1]
979 poss_y = [y - 1, y + 1, y - 1, y, y + 1]
981 # find the point closest to the line
982 true_y = m * poss_x + c
983 diff = (poss_y - true_y) ** 2
984 index = matlab_find(diff == min(diff))[0]
986 # the next point
987 x = poss_x[index[0] - 1]
988 y = poss_y[index[0] - 1]
990 # add the point to the line
991 line[x - 1, y - 1] = 1
993 elif not np.isinf(abs(m)):
995 # start at the end with the smallest value of y
996 if a[1] < b[1]:
997 x = a[0]
998 y = a[1]
999 y_end = b[1]
1000 else:
1001 x = b[0]
1002 y = b[1]
1003 y_end = a[1]
1005 # fill in the first point
1006 line[x, y] = 1
1008 while y < y_end:
1009 # next points to try are
1010 poss_y = [y, y, y + 1, y + 1, y + 1]
1011 poss_x = [x - 1, x + 1, x - 1, x, x + 1]
1013 # find the point closest to the line
1014 true_x = (poss_y - c) / m
1015 diff = (poss_x - true_x) ** 2
1016 index = matlab_find(diff == min(diff))[0]
1018 # the next point
1019 x = poss_x[index[0] - 1]
1020 y = poss_y[index[0] - 1]
1022 # add the point to the line
1023 line[x, y] = 1
1025 else: # m = +-Inf
1027 # start at the end with the smallest value of y
1028 if a[1] < b[1]:
1029 x = a[0]
1030 y = a[1]
1031 y_end = b[1]
1032 else:
1033 x = b[0]
1034 y = b[1]
1035 y_end = a[1]
1037 # fill in the first point
1038 line[x, y] = 1
1040 while y < y_end:
1041 # next point
1042 y = y + 1
1044 # add the point to the line
1045 line[x, y] = 1
1047 # =========================================================================
1048 # CALCULATE AN ANGLED LINE
1049 # =========================================================================
1051 elif linetype == 'angled':
1053 # define an empty grid to hold the line
1054 line = np.zeros((Nx, Ny))
1056 # start at the atart
1057 x, y = startpoint
1059 # fill in the first point
1060 line[x - 1, y - 1] = 1
1062 # initialise the current length of the line
1063 line_length = 0
1065 if abs(angle) == np.pi:
1067 while line_length < linelength:
1069 # next point
1070 y = y + 1
1072 # stop the points incrementing at the edges
1073 if y > Ny:
1074 break
1076 # add the point to the line
1077 line[x - 1, y - 1] = 1
1079 # calculate the current length of the line
1080 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2)
1082 elif (angle < np.pi) and (angle > np.pi / 2):
1084 # define the equation of the line
1085 m = -np.tan(angle - np.pi / 2) # gradient of the line
1086 c = y - m * x # where the line crosses the y axis
1088 while line_length < linelength:
1090 # next points to try are
1091 poss_x = np.array([x - 1, x - 1, x])
1092 poss_y = np.array([y, y + 1, y + 1])
1094 # find the point closest to the line
1095 true_y = m * poss_x + c
1096 diff = (poss_y - true_y) ** 2
1097 index = matlab_find(diff == min(diff))[0]
1099 # the next point
1100 x = poss_x[index[0] - 1]
1101 y = poss_y[index[0] - 1]
1103 # stop the points incrementing at the edges
1104 if (x < 0) or (y > Ny - 1):
1105 break
1107 # add the point to the line
1108 line[x - 1, y - 1] = 1
1110 # calculate the current length of the line
1111 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2)
1113 elif angle == np.pi / 2:
1115 while line_length < linelength:
1117 # next point
1118 x = x - 1
1120 # stop the points incrementing at the edges
1121 if x < 1:
1122 break
1124 # add the point to the line
1125 line[x - 1, y - 1] = 1
1127 # calculate the current length of the line
1128 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2)
1130 elif (angle < np.pi / 2) and (angle > 0):
1132 # define the equation of the line
1133 m = np.tan(np.pi / 2 - angle) # gradient of the line
1134 c = y - m * x # where the line crosses the y axis
1136 while line_length < linelength:
1138 # next points to try are
1139 poss_x = np.array([x - 1, x - 1, x])
1140 poss_y = np.array([y, y - 1, y - 1])
1142 # find the point closest to the line
1143 true_y = m * poss_x + c
1144 diff = (poss_y - true_y) ** 2
1145 index = matlab_find(diff == min(diff))[0]
1147 # the next point
1148 x = poss_x[index[0] - 1]
1149 y = poss_y[index[0] - 1]
1151 # stop the points incrementing at the edges
1152 if (x < 1) or (y < 1):
1153 break
1155 # add the point to the line
1156 line[x - 1, y - 1] = 1
1158 # calculate the current length of the line
1159 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2)
1161 elif angle == 0:
1163 while line_length < linelength:
1165 # next point
1166 y = y - 1
1168 # stop the points incrementing at the edges
1169 if y < 1:
1170 break
1172 # add the point to the line
1173 line[x - 1, y - 1] = 1
1175 # calculate the current length of the line
1176 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2)
1178 elif (angle < 0) and (angle > -np.pi / 2):
1180 # define the equation of the line
1181 m = -np.tan(np.pi / 2 + angle) # gradient of the line
1182 c = y - m * x # where the line crosses the y axis
1184 while line_length < linelength:
1186 # next points to try are
1187 poss_x = np.array([x + 1, x + 1, x])
1188 poss_y = np.array([y, y - 1, y - 1])
1190 # find the point closest to the line
1191 true_y = m * poss_x + c
1192 diff = (poss_y - true_y) ** 2
1193 index = matlab_find(diff == min(diff))[0]
1195 # the next point
1196 x = poss_x[index[0] - 1]
1197 y = poss_y[index[0] - 1]
1199 # stop the points incrementing at the edges
1200 if (x > Nx) or (y < 1):
1201 break
1203 # add the point to the line
1204 line[x - 1, y - 1] = 1
1206 # calculate the current length of the line
1207 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2)
1209 elif angle == -np.pi / 2:
1211 while line_length < linelength:
1213 # next point
1214 x = x + 1
1216 # stop the points incrementing at the edges
1217 if x > Nx:
1218 break
1220 # add the point to the line
1221 line[x - 1, y - 1] = 1
1223 # calculate the current length of the line
1224 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2)
1226 elif (angle < -np.pi / 2) and (angle > -np.pi):
1228 # define the equation of the line
1229 m = np.tan(-angle - np.pi / 2) # gradient of the line
1230 c = y - m * x # where the line crosses the y axis
1232 while line_length < linelength:
1234 # next points to try are
1235 poss_x = np.array([x + 1, x + 1, x])
1236 poss_y = np.array([y, y + 1, y + 1])
1238 # find the point closest to the line
1239 true_y = m * poss_x + c
1240 diff = (poss_y - true_y) ** 2
1241 index = matlab_find(diff == min(diff))[0]
1243 # the next point
1244 x = poss_x[index[0] - 1]
1245 y = poss_y[index[0] - 1]
1247 # stop the points incrementing at the edges
1248 if (x > Nx) or (y > Ny):
1249 break
1251 # add the point to the line
1252 line[x - 1, y - 1] = 1
1254 # calculate the current length of the line
1255 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2)
1257 return line
1260def makeArc(grid_size: np.ndarray, arc_pos: np.ndarray, radius, diameter, focus_pos: np.ndarray):
1261 # force integer input values
1262 grid_size = grid_size.round().astype(int)
1263 arc_pos = arc_pos.round().astype(int)
1264 diameter = int(round(diameter))
1265 focus_pos = focus_pos.round().astype(int)
1267 try:
1268 radius = int(radius)
1269 except OverflowError:
1270 radius = float(radius)
1272 # check the input ranges
1273 if np.any(grid_size < 1):
1274 raise ValueError('The grid size must be positive.')
1275 if radius <= 0:
1276 raise ValueError('The radius must be positive.')
1278 if diameter <= 0:
1279 raise ValueError('The diameter must be positive.')
1281 if np.any(arc_pos < 1) or np.any(arc_pos > grid_size):
1282 raise ValueError('The centre of the arc must be within the grid.')
1284 if diameter > 2 * radius:
1285 raise ValueError('The diameter of the arc must be less than twice the radius of curvature.')
1287 if diameter % 2 != 1:
1288 raise ValueError('The diameter must be an odd number of grid points.')
1290 if np.all(arc_pos == focus_pos):
1291 raise ValueError('The focus_pos must be different to the arc_pos.')
1293 # assign variable names to vector components
1294 Nx, Ny = grid_size
1295 ax, ay = arc_pos
1296 fx, fy = focus_pos
1298 # =========================================================================
1299 # CREATE ARC
1300 # =========================================================================
1302 if not np.isinf(radius):
1304 # find half the arc angle
1305 half_arc_angle = np.arcsin(diameter / 2 / radius)
1307 # find centre of circle on which the arc lies
1308 distance_cf = np.sqrt((ax - fx) ** 2 + (ay - fy) ** 2)
1309 cx = round(radius / distance_cf * (fx - ax) + ax)
1310 cy = round(radius / distance_cf * (fy - ay) + ay)
1311 c = np.array([cx, cy])
1313 # create circle
1314 arc = makeCircle(Nx, Ny, cx, cy, radius)
1316 # form vector from the geometric arc centre to the arc midpoint
1317 v1 = arc_pos - c
1319 # calculate length of vector
1320 l1 = np.sqrt(sum((arc_pos - c) ** 2))
1322 # extract all points that form part of the arc
1323 arc_ind = matlab_find(arc, mode='eq', val=1)
1325 # loop through the arc points
1326 for arc_ind_i in arc_ind:
1328 # extract the indices of the current point
1329 x_ind, y_ind = ind2sub([Nx, Ny], arc_ind_i)
1330 p = np.array([x_ind, y_ind])
1332 # form vector from the geometric arc centre to the current point
1333 v2 = p - c
1335 # calculate length of vector
1336 l2 = np.sqrt(sum((p - c) ** 2))
1338 # find the angle between the two vectors using the dot product,
1339 # normalised using the vector lengths
1340 theta = np.arccos(sum(v1 * v2 / (l1 * l2)))
1342 # if the angle is greater than the half angle of the arc, remove
1343 # it from the arc
1344 if theta > half_arc_angle:
1345 arc = matlab_assign(arc, arc_ind_i - 1, 0)
1346 else:
1348 # calculate arc direction angle, then rotate by 90 degrees
1349 ang = np.arctan((fx - ax) / (fy - ay)) + np.pi / 2
1351 # draw lines to create arc with infinite radius
1352 arc = np.logical_or(
1353 makeLine(Nx, Ny, arc_pos, endpoint=None, angle=ang, length=(diameter - 1) // 2),
1354 makeLine(Nx, Ny, arc_pos, endpoint=None, angle=(ang + np.pi), length=(diameter - 1) // 2)
1355 )
1356 return arc
1359def ind2sub(array_shape, ind):
1360 # Matlab style ind2sub
1361 # row, col = np.unravel_index(ind - 1, array_shape, order='F')
1362 # return np.squeeze(row) + 1, np.squeeze(col) + 1
1364 indices = np.unravel_index(ind - 1, array_shape, order='F')
1365 indices = (np.squeeze(index) + 1 for index in indices)
1366 return indices
1369def sub2ind(array_shape, x, y, z) -> np.ndarray:
1370 results = []
1371 x, y, z = np.squeeze(x), np.squeeze(y), np.squeeze(z)
1372 for x_i, y_i, z_i in zip(x, y, z):
1373 index = np.ravel_multi_index((x_i, y_i, z_i), dims=array_shape, order='F')
1374 results.append(index)
1375 return np.array(results)
1378def makePixelMapPoint(grid_size, centre_pos) -> np.ndarray:
1379 # check for number of dimensions
1380 num_dim = len(grid_size)
1382 # check that centre_pos has the same dimensions
1383 if len(grid_size) != len(centre_pos):
1384 raise ValueError('The inputs centre_pos and grid_size must have the same number of dimensions.')
1386 if num_dim == 2:
1387 # assign inputs and force to be integers
1388 Nx, Ny = grid_size.astype(int)
1389 cx, cy = centre_pos.astype(int)
1391 # generate index vectors in each dimension
1392 nx = np.arange(0, Nx) - cx + 1
1393 ny = np.arange(0, Ny) - cy + 1
1395 # combine index matrices
1396 pixel_map = np.zeros((Nx, Ny))
1397 pixel_map += (nx ** 2)[:, None]
1398 pixel_map += (ny ** 2)[None, :]
1399 pixel_map = np.sqrt(pixel_map)
1401 elif num_dim == 3:
1403 # assign inputs and force to be integers
1404 Nx, Ny, Nz = grid_size.astype(int)
1405 cx, cy, cz = centre_pos.astype(int)
1407 # generate index vectors in each dimension
1408 nx = np.arange(0, Nx) - cx + 1
1409 ny = np.arange(0, Ny) - cy + 1
1410 nz = np.arange(0, Nz) - cz + 1
1412 # combine index matrices
1413 pixel_map = np.zeros((Nx, Ny, Nz))
1414 pixel_map += (nx ** 2)[:, None, None]
1415 pixel_map += (ny ** 2)[None, :, None]
1416 pixel_map += (nz ** 2)[None, None, :]
1417 pixel_map = np.sqrt(pixel_map)
1419 else:
1420 # throw error
1421 raise ValueError('Grid size must be 2 or 3D.')
1423 return pixel_map
1426def makePixelMapPlane(grid_size, normal, point):
1427 # error checking
1428 if np.all(normal == 0):
1429 raise ValueError('Normal vector should not be zero.')
1431 # check for number of dimensions
1432 num_dim = len(grid_size)
1434 if num_dim == 2:
1435 # assign inputs and force to be integers
1436 Nx = round(grid_size[0])
1437 Ny = round(grid_size[1])
1439 # create coordinate meshes
1440 [px, py] = np.meshgrid(Nx, Ny)
1441 [pointx, pointy] = np.meshgrid(np.ones((1, Nx)) * point[0], np.ones(1, Ny) * point[1])
1442 [nx, ny] = np.meshgrid(np.ones((1, Nx)) * normal[0], np.ones(1, Ny) * normal[2])
1444 # calculate distance according to Eq. (6) at
1445 # http://mathworld.wolfram.com/Point-PlaneDistance.html
1446 pixel_map = np.abs((px - pointx) * nx + (py - pointy) * ny) / np.sqrt(sum(normal ** 2))
1448 elif num_dim == 3:
1450 # assign inputs and force to be integers
1451 Nx = np.round(grid_size[0])
1452 Ny = np.round(grid_size[1])
1453 Nz = np.round(grid_size[2])
1455 # create coordinate meshes
1456 px, py, pz = np.meshgrid(np.arange(1, Nx + 1), np.arange(1, Ny + 1), np.arange(1, Nz + 1), indexing='ij')
1457 pointx, pointy, pointz = np.meshgrid(np.ones(Nx) * point[0], np.ones(Ny) * point[1], np.ones(Nz) * point[2],
1458 indexing='ij')
1459 nx, ny, nz = np.meshgrid(np.ones(Nx) * normal[0], np.ones(Ny) * normal[1], np.ones(Nz) * normal[2],
1460 indexing='ij')
1462 # calculate distance according to Eq. (6) at
1463 # http://mathworld.wolfram.com/Point-PlaneDistance.html
1464 pixel_map = np.abs((px - pointx) * nx + (py - pointy) * ny + (pz - pointz) * nz) / np.sqrt(sum(normal ** 2))
1466 else:
1467 # throw error
1468 raise ValueError('Grid size must be 2 or 3D.')
1470 return pixel_map
1473def makeBowl(grid_size, bowl_pos, radius, diameter, focus_pos, binary=False, remove_overlap=False):
1474 # =========================================================================
1475 # DEFINE LITERALS
1476 # =========================================================================
1478 # threshold used to find the closest point to the radius
1479 THRESHOLD = 0.5
1481 # number of grid points to expand the bounding box compared to
1482 # sqrt(2)*diameter
1483 BOUNDING_BOX_EXP = 2
1485 # =========================================================================
1486 # INPUT CHECKING
1487 # =========================================================================
1489 # force integer input values
1490 grid_size = np.round(grid_size).astype(int)
1491 bowl_pos = np.round(bowl_pos).astype(int)
1492 focus_pos = np.round(focus_pos).astype(int)
1493 diameter = np.round(diameter)
1494 radius = np.round(radius)
1496 # check the input ranges
1497 if np.any(grid_size < 1):
1498 raise ValueError('The grid size must be positive.')
1499 if np.any(bowl_pos < 1) or np.any(bowl_pos > grid_size):
1500 raise ValueError('The centre of the bowl must be within the grid.')
1501 if radius <= 0:
1502 raise ValueError('The radius must be positive.')
1503 if diameter <= 0:
1504 raise ValueError('The diameter must be positive.')
1505 if diameter > (2 * radius):
1506 raise ValueError('The diameter of the bowl must be less than twice the radius of curvature.')
1507 if diameter % 2 == 0:
1508 raise ValueError('The diameter must be an odd number of grid points.')
1509 if np.all(bowl_pos == focus_pos):
1510 raise ValueError('The focus_pos must be different to the bowl_pos.')
1512 # =========================================================================
1513 # BOUND THE GRID TO SPEED UP CALCULATION
1514 # =========================================================================
1516 # create bounding box slightly larger than bowl diameter * sqrt(2)
1517 Nx = np.round(np.sqrt(2) * diameter).astype(int) + BOUNDING_BOX_EXP
1518 Ny = Nx
1519 Nz = Nx
1520 grid_size_sm = np.array([Nx, Ny, Nz])
1522 # set the bowl position to be the centre of the bounding box
1523 bx = np.ceil(Nx / 2).astype(int)
1524 by = np.ceil(Ny / 2).astype(int)
1525 bz = np.ceil(Nz / 2).astype(int)
1526 bowl_pos_sm = np.array([bx, by, bz])
1528 # set the focus position to be in the direction specified by the user
1529 fx = bx + (focus_pos[0] - bowl_pos[0])
1530 fy = by + (focus_pos[1] - bowl_pos[1])
1531 fz = bz + (focus_pos[2] - bowl_pos[2])
1532 focus_pos_sm = [fx, fy, fz]
1534 # preallocate storage variable
1535 if binary:
1536 bowl_sm = np.zeros((Nx, Ny, Nz), dtype=bool)
1537 else:
1538 bowl_sm = np.zeros((Nx, Ny, Nz))
1540 # =========================================================================
1541 # CREATE DISTANCE MATRIX
1542 # =========================================================================
1544 if not np.isinf(radius):
1546 # find half the arc angle
1547 half_arc_angle = np.arcsin(diameter / (2 * radius))
1549 # find centre of sphere on which the bowl lies
1550 distance_cf = np.sqrt((bx - fx) ** 2 + (by - fy) ** 2 + (bz - fz) ** 2)
1551 cx = round(radius / distance_cf * (fx - bx) + bx)
1552 cy = round(radius / distance_cf * (fy - by) + by)
1553 cz = round(radius / distance_cf * (fz - bz) + bz)
1554 c = np.array([cx, cy, cz])
1556 # generate matrix with distance from the centre
1557 pixel_map = makePixelMapPoint(grid_size_sm, c)
1559 # set search radius to bowl radius
1560 search_radius = radius
1562 else:
1564 # generate matrix with distance from the centre
1565 pixel_map = makePixelMapPlane(grid_size_sm, bowl_pos_sm - focus_pos_sm, bowl_pos_sm)
1567 # set search radius to 0 (the disc is flat)
1568 search_radius = 0
1570 # calculate distance from search radius
1571 pixel_map = np.abs(pixel_map - search_radius)
1573 # =========================================================================
1574 # DIMENSION 1
1575 # =========================================================================
1577 # find the grid point that corresponds to the outside of the bowl in the
1578 # first dimension in both directions (the index gives the distance along
1579 # this dimension)
1580 value_forw, index_forw = pixel_map.min(axis=0), pixel_map.argmin(axis=0)
1581 value_back, index_back = np.flip(pixel_map, axis=0).min(axis=0), np.flip(pixel_map, axis=0).argmin(axis=0)
1583 # extract the linear index in the y-z plane of the values that lie on the
1584 # bowl surface
1585 yz_ind_forw = matlab_find(value_forw < THRESHOLD)
1586 yz_ind_back = matlab_find(value_back < THRESHOLD)
1588 # use these subscripts to extract the x-index of the grid points that lie
1589 # on the bowl surface
1590 x_ind_forw = index_forw.flatten(order='F')[yz_ind_forw - 1] + 1
1591 x_ind_back = index_back.flatten(order='F')[yz_ind_back - 1] + 1
1593 # convert the linear index to equivalent subscript values
1594 y_ind_forw, z_ind_forw = ind2sub([Ny, Nz], yz_ind_forw)
1595 y_ind_back, z_ind_back = ind2sub([Ny, Nz], yz_ind_back)
1597 # combine x-y-z indices into a linear index
1598 linear_index_forw = sub2ind([Nx, Ny, Nz], x_ind_forw - 1, y_ind_forw - 1, z_ind_forw - 1) + 1
1599 linear_index_back = sub2ind([Nx, Ny, Nz], Nx - x_ind_back, y_ind_back - 1, z_ind_back - 1) + 1
1601 # assign these values to the bowl
1602 bowl_sm = matlab_assign(bowl_sm, linear_index_forw - 1, 1)
1603 bowl_sm = matlab_assign(bowl_sm, linear_index_back - 1, 1)
1605 # set existing bowl values to a distance of zero in the pixel map (this
1606 # avoids problems with overlapping pixels)
1607 pixel_map[bowl_sm == 1] = 0
1609 # =========================================================================
1610 # DIMENSION 2
1611 # =========================================================================
1613 # find the grid point that corresponds to the outside of the bowl in the
1614 # second dimension in both directions (the pixel map is first re-ordered to
1615 # [X, Y, Z] -> [Y, Z, X])
1616 pixel_map_temp = np.transpose(pixel_map, (1, 2, 0))
1617 value_forw, index_forw = pixel_map_temp.min(axis=0), pixel_map_temp.argmin(axis=0)
1618 value_back, index_back = np.flip(pixel_map_temp, axis=0).min(axis=0), np.flip(pixel_map_temp, axis=0).argmin(axis=0)
1619 del pixel_map_temp
1621 # extract the linear index in the y-z plane of the values that lie on the
1622 # bowl surface
1623 zx_ind_forw = matlab_find(value_forw < THRESHOLD)
1624 zx_ind_back = matlab_find(value_back < THRESHOLD)
1626 # use these subscripts to extract the y-index of the grid points that lie
1627 # on the bowl surface
1628 y_ind_forw = index_forw.flatten(order='F')[zx_ind_forw - 1] + 1
1629 y_ind_back = index_back.flatten(order='F')[zx_ind_back - 1] + 1
1631 # convert the linear index to equivalent subscript values
1632 z_ind_forw, x_ind_forw = ind2sub([Nz, Nx], zx_ind_forw)
1633 z_ind_back, x_ind_back = ind2sub([Nz, Nx], zx_ind_back)
1635 # combine x-y-z indices into a linear index
1636 linear_index_forw = sub2ind([Nx, Ny, Nz], x_ind_forw - 1, y_ind_forw - 1, z_ind_forw - 1) + 1
1637 linear_index_back = sub2ind([Nx, Ny, Nz], x_ind_back - 1, Ny - y_ind_back, z_ind_back - 1) + 1
1639 # assign these values to the bowl
1640 bowl_sm = matlab_assign(bowl_sm, linear_index_forw - 1, 1)
1641 bowl_sm = matlab_assign(bowl_sm, linear_index_back - 1, 1)
1643 # set existing bowl values to a distance of zero in the pixel map (this
1644 # avoids problems with overlapping pixels)
1645 pixel_map[bowl_sm == 1] = 0
1647 # =========================================================================
1648 # DIMENSION 3
1649 # =========================================================================
1651 # find the grid point that corresponds to the outside of the bowl in the
1652 # third dimension in both directions (the pixel map is first re-ordered to
1653 # [X, Y, Z] -> [Z, X, Y])
1654 pixel_map_temp = np.transpose(pixel_map, (2, 0, 1))
1655 value_forw, index_forw = pixel_map_temp.min(axis=0), pixel_map_temp.argmin(axis=0)
1656 value_back, index_back = np.flip(pixel_map_temp, axis=0).min(axis=0), np.flip(pixel_map_temp, axis=0).argmin(axis=0)
1657 del pixel_map_temp
1659 # extract the linear index in the y-z plane of the values that lie on the
1660 # bowl surface
1661 xy_ind_forw = matlab_find(value_forw < THRESHOLD)
1662 xy_ind_back = matlab_find(value_back < THRESHOLD)
1664 # use these subscripts to extract the z-index of the grid points that lie
1665 # on the bowl surface
1666 z_ind_forw = index_forw.flatten(order='F')[xy_ind_forw - 1] + 1
1667 z_ind_back = index_back.flatten(order='F')[xy_ind_back - 1] + 1
1669 # convert the linear index to equivalent subscript values
1670 x_ind_forw, y_ind_forw = ind2sub([Nx, Ny], xy_ind_forw)
1671 x_ind_back, y_ind_back = ind2sub([Nx, Ny], xy_ind_back)
1673 # combine x-y-z indices into a linear index
1674 linear_index_forw = sub2ind([Nx, Ny, Nz], x_ind_forw - 1, y_ind_forw - 1, z_ind_forw - 1) + 1
1675 linear_index_back = sub2ind([Nx, Ny, Nz], x_ind_back - 1, y_ind_back - 1, Nz - z_ind_back) + 1
1677 # assign these values to the bowl
1678 bowl_sm = matlab_assign(bowl_sm, linear_index_forw - 1, 1)
1679 bowl_sm = matlab_assign(bowl_sm, linear_index_back - 1, 1)
1681 # =========================================================================
1682 # RESTRICT SPHERE TO BOWL
1683 # =========================================================================
1685 # remove grid points within the sphere that do not form part of the bowl
1686 if not np.isinf(radius):
1688 # form vector from the geometric bowl centre to the back of the bowl
1689 v1 = bowl_pos_sm - c
1691 # calculate length of vector
1692 l1 = np.sqrt(sum((bowl_pos_sm - c) ** 2))
1694 # loop through the non-zero elements in the bowl matrix
1695 bowl_ind = matlab_find(bowl_sm == 1)[:, 0]
1696 for bowl_ind_i in bowl_ind:
1698 # extract the indices of the current point
1699 x_ind, y_ind, z_ind = ind2sub([Nx, Ny, Nz], bowl_ind_i)
1700 p = np.array([x_ind, y_ind, z_ind])
1702 # form vector from the geometric bowl centre to the current point
1703 # on the bowl
1704 v2 = p - c
1706 # calculate length of vector
1707 l2 = np.sqrt(sum((p - c) ** 2))
1709 # find the angle between the two vectors using the dot product,
1710 # normalised using the vector lengths
1711 theta = np.arccos(sum(v1 * v2 / (l1 * l2)))
1713 # # alternative calculation normalised using radius of curvature
1714 # theta2 = acos(sum( v1 .* v2 ./ radius**2 ))
1716 # if the angle is greater than the half angle of the bowl, remove
1717 # it from the bowl
1718 if theta > half_arc_angle:
1719 bowl_sm = matlab_assign(bowl_sm, bowl_ind_i - 1, 0)
1721 else:
1723 # form a distance map from the centre of the disc
1724 pixelMapPoint = makePixelMapPoint(grid_size_sm, bowl_pos_sm)
1726 # set all points in the disc greater than the diameter to zero
1727 bowl_sm[pixelMapPoint > (diameter / 2)] = 0
1729 # =========================================================================
1730 # REMOVE OVERLAPPED POINTS
1731 # =========================================================================
1733 if remove_overlap:
1735 # define the shapes that capture the overlapped points, along with the
1736 # corresponding mask of which point to delete
1737 overlap_shapes = []
1738 overlap_delete = []
1740 shape = np.zeros((3, 3, 3))
1741 shape[0, 0, :] = 1
1742 shape[1, 1, :] = 1
1743 shape[2, 2, :] = 1
1744 shape[0, 1, 1] = 1
1745 shape[1, 2, 1] = 1
1746 overlap_shapes.append(shape)
1748 delete = np.zeros((3, 3, 3))
1749 delete[0, 1, 1] = 1
1750 delete[0, 2, 1] = 1
1751 overlap_delete.append(delete)
1753 shape = np.zeros((3, 3, 3))
1754 shape[0, 0, :] = 1
1755 shape[1, 1, :] = 1
1756 shape[2, 2, :] = 1
1757 shape[0, 1, 1] = 1
1758 overlap_shapes.append(shape)
1760 delete = np.zeros((3, 3, 3))
1761 delete[0, 1, 1] = 1
1762 overlap_delete.append(delete)
1764 shape = np.zeros((3, 3, 3))
1765 shape[0:2, 0, :] = 1
1766 shape[2, 1, :] = 1
1767 shape[1, 1, 1] = 1
1768 overlap_shapes.append(shape)
1770 delete = np.zeros((3, 3, 3))
1771 delete[1, 1, 1] = 1
1772 overlap_delete.append(delete)
1774 shape = np.zeros((3, 3, 3))
1775 shape[0, 0, :] = 1
1776 shape[1, 1, :] = 1
1777 shape[2, 2, :] = 1
1778 shape[0, 1, 0] = 1
1779 overlap_shapes.append(shape)
1781 delete = np.zeros((3, 3, 3))
1782 delete[0, 1, 0] = 1
1783 overlap_delete.append(delete)
1785 shape = np.zeros((3, 3, 3))
1786 shape[0:2, 1, :] = 1
1787 shape[2, 2, :] = 1
1788 shape[2, 1, 0] = 1
1789 overlap_shapes.append(shape)
1791 delete = np.zeros((3, 3, 3))
1792 delete[2, 1, 0] = 1
1793 overlap_delete.append(delete)
1795 shape = np.zeros((3, 3, 3))
1796 shape[0, :, 2] = 1
1797 shape[1, :, 1] = 1
1798 shape[1, :, 0] = 1
1799 shape[2, 1, 0] = 1
1800 overlap_shapes.append(shape)
1802 delete = np.zeros((3, 3, 3))
1803 delete[2, 1, 0] = 1
1804 overlap_delete.append(delete)
1806 shape = np.zeros((3, 3, 3))
1807 shape[0, 2, :] = 1
1808 shape[1, 0:2, :] = 1
1809 shape[2, 0, 0] = 1
1810 overlap_shapes.append(shape)
1812 delete = np.zeros((3, 3, 3))
1813 delete[2, 0, 0] = 1
1814 overlap_delete.append(delete)
1816 shape = np.zeros((3, 3, 3))
1817 shape[:, :, 1] = 1
1818 shape[0, 0, 0] = 1
1819 overlap_shapes.append(shape)
1821 delete = np.zeros((3, 3, 3))
1822 delete[0, 0, 0] = 1
1823 overlap_delete.append(delete)
1825 shape = np.zeros((3, 3, 3))
1826 shape[0, :, 0] = 1
1827 shape[1, :, 1] = 1
1828 shape[1, :, 2] = 1
1829 shape[1, 1, 0] = 1
1830 overlap_shapes.append(shape)
1832 delete = np.zeros((3, 3, 3))
1833 delete[1, 1, 0] = 1
1834 overlap_delete.append(delete)
1836 shape = np.zeros((3, 3, 3))
1837 shape[1:3, 2, 0] = 1
1838 shape[0, 2, 1:3] = 1
1839 shape[0, 1, 2] = 1
1840 shape[1, 1, 1] = 1
1841 shape[2, 1, 0] = 1
1842 shape[1:3, 0, 1] = 1
1843 shape[1, 0, 2] = 1
1844 overlap_shapes.append(shape)
1846 delete = np.zeros((3, 3, 3))
1847 delete[1, 0, 1] = 1
1848 overlap_delete.append(delete)
1850 # set loop flag
1851 points_remaining = True
1853 # initialise deleted point counter
1854 deleted_points = 0
1856 # set list of possible permutations
1857 perm_list = [
1858 [0, 1, 2],
1859 [0, 2, 1],
1860 [1, 2, 0],
1861 [1, 0, 2],
1862 [2, 0, 1],
1863 [2, 1, 0]
1864 ]
1866 while points_remaining:
1868 # get linear index of non-zero bowl elements
1869 index_mat = matlab_find(bowl_sm > 0)[:, 0]
1871 # set Boolean delete variable
1872 delete_point = False
1874 # loop through all points on the bowl, and find the all the points with
1875 # more than 8 neighbours
1876 index = 0
1877 for index, index_mat_i in enumerate(index_mat):
1879 # extract subscripts for current point
1880 cx, cy, cz = ind2sub([Nx, Ny, Nz], index_mat_i)
1882 # ignore edge points
1883 if (cx > 1) and (cx < Nx) and (cy > 1) and (cy < Ny) and (cz > 1) and (cz < Nz):
1885 # extract local region around current point
1886 region = bowl_sm[cx - 1:cx + 1, cy - 1:cy + 1, cz - 1:cz + 1] # FARID might not work
1888 # if there's more than 8 neighbours, check the point for
1889 # deletion
1890 if (region.sum() - 1) > 8:
1892 # loop through the different shapes
1893 for shape_index in range(len(overlap_shapes)):
1895 # check every permutation of the shape, and apply the
1896 # deletion mask if the pattern matches
1898 # loop through possible shape permutations
1899 for ind1 in range(len(perm_list)):
1901 # get shape and delete mask
1902 overlap_s = overlap_shapes[shape_index]
1903 overlap_d = overlap_delete[shape_index]
1905 # permute
1906 overlap_s = np.transpose(overlap_s, perm_list[ind1])
1907 overlap_d = np.transpose(overlap_d, perm_list[ind1])
1909 # loop through possible shape reflections
1910 for ind2 in range(1, 8):
1912 # flipfunc the shape
1913 if ind2 == 2:
1914 overlap_s = np.flip(overlap_s, axis=0)
1915 overlap_d = np.flip(overlap_d, axis=0)
1916 elif ind2 == 3:
1917 overlap_s = np.flip(overlap_s, axis=1)
1918 overlap_d = np.flip(overlap_d, axis=1)
1919 elif ind2 == 4:
1920 overlap_s = np.flip(overlap_s, axis=2)
1921 overlap_d = np.flip(overlap_d, axis=2)
1922 elif ind2 == 5:
1923 overlap_s = np.flip(np.flip(overlap_s, axis=0), axis=1)
1924 overlap_d = np.flip(np.flip(overlap_d, axis=0), axis=1)
1925 elif ind2 == 6:
1926 overlap_s = np.flip(np.flip(overlap_s, axis=0), axis=2)
1927 overlap_d = np.flip(np.flip(overlap_d, axis=0), axis=2)
1928 elif ind2 == 7:
1929 overlap_s = np.flip(np.flip(overlap_s, axis=1), axis=2)
1930 overlap_d = np.flip(np.flip(overlap_d, axis=1), axis=2)
1932 # rotate the shape 4 x 90 degrees
1933 for ind3 in range(4):
1935 # check if the shape matches
1936 if np.all(overlap_s == region):
1937 delete_point = True
1939 # break from loop if a match is found
1940 if delete_point:
1941 break
1943 # rotate shape
1944 overlap_s = np.rot90(overlap_s)
1945 overlap_d = np.rot90(overlap_d)
1947 # break from loop if a match is found
1948 if delete_point:
1949 break
1951 # break from loop if a match is found
1952 if delete_point:
1953 break
1955 # remove point from bowl if required, and update
1956 # counter
1957 if delete_point:
1958 bowl_sm[cx - 1:cx + 1, cy - 1:cy + 1, cz - 1:cz + 1] = bowl_sm[cx - 1:cx + 1,
1959 cy - 1:cy + 1,
1960 cz - 1:cz + 1] * np.bitwise_not(
1961 overlap_d).astype(float) # Farid won't work probably
1962 deleted_points = deleted_points + 1
1963 break
1965 # break from loop if a match is found
1966 if delete_point:
1967 break
1969 # break from while loop if the outer for loop has completed
1970 # without deleting a point
1971 if index == (len(index_mat) - 1):
1972 points_remaining = False
1974 # display status
1975 if deleted_points:
1976 print('{deleted_points} overlapped points removed from bowl')
1978 # =========================================================================
1979 # PLACE BOWL WITHIN LARGER GRID
1980 # =========================================================================
1982 # preallocate storage variable
1983 if binary:
1984 bowl = np.zeros(grid_size, dtype=bool)
1985 else:
1986 bowl = np.zeros(grid_size)
1988 # calculate position of bounding box within larger grid
1989 x1 = bowl_pos[0] - bx
1990 x2 = x1 + Nx
1991 y1 = bowl_pos[1] - by
1992 y2 = y1 + Ny
1993 z1 = bowl_pos[2] - bz
1994 z2 = z1 + Nz
1996 # truncate bounding box if it falls outside the grid
1997 if x1 < 0:
1998 bowl_sm = bowl_sm[abs(x1):, :, :]
1999 x1 = 0
2000 if y1 < 0:
2001 bowl_sm = bowl_sm[:, abs(y1):, :]
2002 y1 = 0
2003 if z1 < 0:
2004 bowl_sm = bowl_sm[:, :, abs(z1):]
2005 z1 = 0
2006 if x2 >= grid_size[0]:
2007 to_delete = x2 - grid_size[0]
2008 bowl_sm = bowl_sm[:-to_delete, :, :]
2009 x2 = grid_size[0]
2010 if y2 >= grid_size[1]:
2011 to_delete = y2 - grid_size[1]
2012 bowl_sm = bowl_sm[:, :-to_delete, :]
2013 y2 = grid_size[1]
2014 if z2 >= grid_size[2]:
2015 to_delete = z2 - grid_size[2]
2016 bowl_sm = bowl_sm[:, :, :-to_delete]
2017 z2 = grid_size[2]
2019 # place bowl into grid
2020 bowl[x1:x2, y1:y2, z1:z2] = bowl_sm
2022 return bowl
2025def makeMultiBowl(grid_size, bowl_pos, radius, diameter, focus_pos, binary=False, remove_overlap=False):
2026 # =========================================================================
2027 # DEFINE LITERALS
2028 # =========================================================================
2030 # =========================================================================
2031 # INPUT CHECKING
2032 # =========================================================================
2034 # check inputs
2035 if bowl_pos.shape[-1] != 3:
2036 raise ValueError('bowl_pos should contain 3 columns, with [bx, by, bz] in each row.')
2038 if len(radius) != 1 and len(radius) != bowl_pos.shape[0]:
2039 raise ValueError('The number of rows in bowl_pos and radius does not match.')
2041 if len(diameter) != 1 and len(diameter) != bowl_pos.shape[0]:
2042 raise ValueError('The number of rows in bowl_pos and diameter does not match.')
2044 # force integer grid size values
2045 grid_size = np.round(grid_size).astype(int)
2046 bowl_pos = np.round(bowl_pos).astype(int)
2047 focus_pos = np.round(focus_pos).astype(int)
2048 diameter = np.round(diameter)
2049 radius = np.round(radius)
2051 # =========================================================================
2052 # CREATE BOWLS
2053 # =========================================================================
2055 # preallocate output matrices
2056 if binary:
2057 bowls = np.zeros(grid_size, dtype=bool)
2058 else:
2059 bowls = np.zeros(grid_size)
2061 bowls_labelled = np.zeros(grid_size)
2063 # loop for calling makeBowl
2064 for bowl_index in range(bowl_pos.shape[0]):
2066 # update command line status
2067 if bowl_index == 1:
2068 TicToc.tic()
2069 else:
2070 TicToc.toc(reset=True)
2071 print(f'Creating bowl {bowl_index} of {bowl_pos.shape[0]} ... ')
2073 # get parameters for current bowl
2074 if bowl_pos.shape[0] > 1:
2075 bowl_pos_k = bowl_pos[bowl_index]
2076 else:
2077 bowl_pos_k = bowl_pos
2079 if len(radius) > 1:
2080 radius_k = radius[bowl_index]
2081 else:
2082 radius_k = radius
2084 if len(diameter) > 1:
2085 diameter_k = diameter[bowl_index]
2086 else:
2087 diameter_k = diameter
2089 if focus_pos.shape[0] > 1:
2090 focus_pos_k = focus_pos[bowl_index]
2091 else:
2092 focus_pos_k = focus_pos
2094 # create new bowl
2095 new_bowl = makeBowl(
2096 grid_size, bowl_pos_k, radius_k, diameter_k, focus_pos_k,
2097 remove_overlap=remove_overlap, binary=binary
2098 )
2100 # add bowl to bowl matrix
2101 bowls = bowls + new_bowl
2103 # add new bowl to labelling matrix
2104 bowls_labelled[new_bowl == 1] = bowl_index
2106 TicToc.toc()
2108 # check if any of the bowls are overlapping
2109 max_nd_val, _ = max_nd(bowls)
2110 if max_nd_val > 1:
2111 # display warning
2112 print(f'WARNING: {max_nd_val - 1} bowls are overlapping')
2114 # force the output to be binary
2115 bowls[bowls != 0] = 1
2117 return bowls, bowls_labelled
2120def makeMultiArc(grid_size: np.ndarray, arc_pos: np.ndarray, radius, diameter, focus_pos: np.ndarray):
2121 # check inputs
2122 if arc_pos.shape[-1] != 2:
2123 raise ValueError('arc_pos should contain 2 columns, with [ax, ay] in each row.')
2125 if len(radius) != 1 and len(radius) != arc_pos.shape[0]:
2126 raise ValueError('The number of rows in arc_pos and radius does not match.')
2128 if len(diameter) != 1 and len(diameter) != arc_pos.shape[0]:
2129 raise ValueError('The number of rows in arc_pos and diameter does not match.')
2131 # force integer grid size values
2132 grid_size = grid_size.round().astype(int)
2133 arc_pos = arc_pos.round().astype(int)
2134 diameter = diameter.round()
2135 focus_pos = focus_pos.round().astype(int)
2136 radius = radius.round()
2138 # =========================================================================
2139 # CREATE ARCS
2140 # =========================================================================
2142 # create empty matrix
2143 arcs = np.zeros(grid_size)
2144 arcs_labelled = np.zeros(grid_size)
2146 # loop for calling makeArc
2147 for k in range(arc_pos.shape[0]):
2149 # get parameters for current arc
2150 if arc_pos.shape[0] > 1:
2151 arc_pos_k = arc_pos[k]
2152 else:
2153 arc_pos_k = arc_pos
2155 if len(radius) > 1:
2156 radius_k = radius[k]
2157 else:
2158 radius_k = radius
2160 if len(diameter) > 1:
2161 diameter_k = diameter[k]
2162 else:
2163 diameter_k = diameter
2165 if focus_pos.shape[0] > 1:
2166 focus_pos_k = focus_pos[k]
2167 else:
2168 focus_pos_k = focus_pos
2170 # create new arc
2171 new_arc = makeArc(grid_size, arc_pos_k, radius_k, diameter_k, focus_pos_k)
2173 # add arc to arc matrix
2174 arcs = arcs + new_arc
2176 # add new arc to labelling matrix
2177 arcs_labelled[new_arc == 1] = k
2179 # check if any of the arcs are overlapping
2180 max_nd_val, _ = max_nd(arcs)
2181 if max_nd_val > 1:
2182 # display warning
2183 print(f'WARNING: {max_nd_val - 1} arcs are overlapping')
2185 # force the output to be binary
2186 arcs[arcs != 0] = 1
2188 return arcs, arcs_labelled
2191def makeSphere(Nx, Ny, Nz, radius, plot_sphere=False, binary=False):
2193 # enforce a centered sphere
2194 cx = floor(Nx / 2) + 1
2195 cy = floor(Ny / 2) + 1
2196 cz = floor(Nz / 2) + 1
2198 # preallocate the storage variable
2199 if binary:
2200 sphere = np.zeros((Nx, Ny, Nz), dtype=bool)
2201 else:
2202 sphere = np.zeros((Nx, Ny, Nz))
2204 # create a guide circle from which the individal radii can be extracted
2205 guide_circle = makeCircle(Ny, Nx, cy, cx, radius)
2207 # step through the guide circle points and create partially filled discs
2208 centerpoints = np.arange(cx - radius, cx + 1)
2209 reflection_offset = np.arange(len(centerpoints), 1, -1)
2210 for centerpoint_index in range(len(centerpoints)):
2212 # extract the current row from the guide circle
2213 row_data = guide_circle[:, centerpoints[centerpoint_index] - 1]
2215 # add an index to the grid points in the current row
2216 row_index = row_data * np.arange(1, len(row_data) + 1)
2218 # calculate the radius
2219 swept_radius = (row_index.max() - row_index[row_index != 0].min()) / 2
2221 # create a circle to add to the sphere
2222 circle = makeCircle(Ny, Nz, cy, cz, swept_radius)
2224 # make an empty fill matrix
2225 if binary:
2226 circle_fill = np.zeros((Ny, Nz), dtype=bool)
2227 else:
2228 circle_fill = np.zeros((Ny, Nz))
2230 # fill in the circle line by line
2231 fill_centerpoints = np.arange(cz - swept_radius, cz + swept_radius + 1).astype(int)
2232 for fill_centerpoints_i in fill_centerpoints:
2234 # extract the first row
2235 row_data = circle[:, fill_centerpoints_i - 1]
2237 # add an index to the grid points in the current row
2238 row_index = row_data * np.arange(1, len(row_data) + 1)
2240 # calculate the diameter
2241 start_index = row_index[row_index != 0].min()
2242 stop_index = row_index.max()
2244 # count how many points on the line
2245 num_points = sum(row_data)
2247 # fill in the line
2248 if start_index != stop_index and (stop_index - start_index) >= num_points:
2249 circle_fill[(start_index + num_points // 2) - 1:stop_index - (num_points // 2), fill_centerpoints_i - 1] = 1
2251 # remove points from the filled circle that existed in the previous
2252 # layer
2253 if centerpoint_index == 0:
2254 sphere[centerpoints[centerpoint_index] - 1, :, :] = circle + circle_fill
2255 prev_circle = circle + circle_fill
2256 else:
2257 prev_circle_alt = circle + circle_fill
2258 circle_fill = circle_fill - prev_circle
2259 circle_fill[circle_fill < 0] = 0
2260 sphere[centerpoints[centerpoint_index] - 1, :, :] = circle + circle_fill
2261 prev_circle = prev_circle_alt
2263 # create the other half of the sphere at the same time
2264 if centerpoint_index != len(centerpoints) - 1:
2265 sphere[cx + reflection_offset[centerpoint_index] - 2, :, :] = sphere[centerpoints[centerpoint_index] - 1, :, :]
2267 # plot results
2268 if plot_sphere:
2269 raise NotImplementedError
2270 return sphere
2273def makeSphericalSection(radius, height, width=None, plot_section=False, binary=False):
2274 use_spherical_sections = True
2276 # force inputs to be integers
2277 radius = int(radius)
2278 height = int(height)
2280 use_width = (width is not None)
2281 if use_width:
2282 width = int(width)
2283 if width % 2 == 0:
2284 raise ValueError('Input width must be an odd number.')
2286 # calculate minimum grid dimensions to fit entire sphere
2287 Nx = 2*radius + 1
2289 # create sphere
2290 ss = makeSphere(Nx, Nx, Nx, radius, False, binary)
2292 # truncate to given height
2293 if use_spherical_sections:
2294 ss = ss[:height, :, :]
2295 else:
2296 ss = np.transpose(ss[:, :height, :], [1, 2, 0])
2298 # flatten transducer and store the maximum and indices
2299 mx = np.squeeze(np.max(ss, axis=0))
2301 # calculate the total length/width of the transducer
2302 length = mx[(len(mx) + 1) // 2].sum()
2304 # truncate transducer grid based on length (removes empty rows and columns)
2305 offset = int((Nx - length)/2)
2306 ss = ss[:, offset:-offset, offset:-offset]
2308 # also truncate to given width if defined by user
2309 if use_width:
2311 # check the value is appropriate
2312 if width > length:
2313 raise ValueError('Input for width must be less than or equal to transducer length.')
2315 # calculate offset
2316 offset = int((length - width)/2)
2318 # truncate transducer grid
2319 ss = ss[:, offset:-offset, :]
2321 # compute average distance between each grid point and its contiguous
2323 # calculate x-index of each grid point in the spherical section, create
2324 # mask and remove singleton dimensions
2325 mx, mx_ind = np.max(ss, axis=0), ss.argmax(axis=0) + 1
2326 mask = np.squeeze(mx != 0)
2327 mx_ind = np.squeeze(mx_ind) * mask
2329 # double check there there is only one value of spherical section in
2330 # each matrix column
2331 if mx.sum() != ss.sum():
2332 raise ValueError('mean neighbour distance cannot be calculated uniquely due to overlapping points in the x-direction')
2334 # calculate average distance to grid point neighbours in the flat case
2335 x_dist = np.tile([1, 0, 1], [3, 1])
2336 y_dist = x_dist.T
2337 flat_dist = np.sqrt(x_dist**2 + y_dist**2)
2338 flat_dist = np.mean(flat_dist)
2340 # compute distance map
2341 dist_map = np.zeros(mx_ind.shape)
2342 sz = mx_ind.shape
2343 for m in range(sz[0]):
2344 for n in range(sz[1]):
2346 # clear map
2347 local_heights = np.zeros((3, 3))
2349 # extract the height (x-distance) of the 8 neighbouring grid
2350 # points
2351 if m == 0 and n == 0:
2352 local_heights[1:3, 1:3] = mx_ind[m:m + 2, n:n + 2]
2353 elif m == (sz[0] - 1) and n == (sz[1] - 1):
2354 local_heights[0:2, 0:2] = mx_ind[m - 1:m+1, n - 1:n+1]
2355 elif m == 0 and n == (sz[1] - 1):
2356 local_heights[1:3, 0:2] = mx_ind[m:m + 2, n - 1:n + 1]
2357 elif m == (sz[0] - 1) and n == 0:
2358 local_heights[0:2, 1:3] = mx_ind[m - 1:m+1, n:n + 2]
2359 elif m == 0:
2360 local_heights[1:3, :] = mx_ind[m:m + 2, n - 1:n + 2]
2361 elif m == (sz[0] - 1):
2362 local_heights[0:2, :] = mx_ind[m - 1:m+1, n - 1:n + 2]
2363 elif n == 0:
2364 local_heights[:, 1:3] = mx_ind[m - 1:m + 2, n:n + 2]
2365 elif n == (sz[1] - 1):
2366 local_heights[:, 0:2] = mx_ind[m - 1:m + 2, n - 1:n + 1]
2367 else:
2368 local_heights = mx_ind[m - 1:m + 2, n - 1:n + 2]
2370 # compute average variation from center
2371 local_heights_var = abs(local_heights - local_heights[1, 1])
2373 # threshold no neighbours
2374 local_heights_var[local_heights == 0] = 0
2376 # calculate total distance from centre
2377 dist = np.sqrt(x_dist**2 + y_dist**2 + local_heights_var**2)
2379 # average and store as a ratio
2380 dist_map[m, n] = 1 + (np.mean(dist) - flat_dist) / flat_dist
2382 # threshold out the non-transducer grid points
2383 dist_map[mask != 1] = 0
2385 # plot if required
2386 if plot_section:
2387 raise NotImplementedError
2389 return ss, dist_map