BOSlib 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
BOSlib/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ from .shift import *
2
+ from .shift_utils import *
3
+ from .utils import *
4
+ from .reconstruction import *
5
+ from .reconstruction_utils import *
6
+ from .culculate_refractiveindex import *
7
+ from .evaluation import *
8
+
9
+ __version__ = '0.0.1'
@@ -0,0 +1,89 @@
1
+ from tqdm.contrib import tenumerate
2
+ from tqdm import tqdm
3
+ import numpy as np
4
+
5
+ def SOR_2D(array_laplacian: np.ndarray,omega_SOR: float,e: float,tolerance:float =1e-24,max_stable_iters:int=1000000):
6
+ """
7
+ Performs the Successive Over-Relaxation (SOR) method on a 2D Laplacian array to solve for steady-state solutions
8
+ within each slice of the array.
9
+
10
+ Parameters
11
+ ----------
12
+ array_laplacian : np.ndarray
13
+ A 3D numpy array where each slice represents a 2D Laplacian grid to be solved.
14
+ omega_SOR : float
15
+ The relaxation factor for the SOR method. Values between 1 and 2 can speed up convergence.
16
+ e : float
17
+ The convergence tolerance threshold for each slice. Iterations stop once the change (`delta`)
18
+ falls below this threshold.
19
+ tolerance : float, optional
20
+ The tolerance level to determine stability in convergence. Defaults to 1e-24.
21
+ max_stable_iters : int, optional
22
+ The maximum number of stable iterations allowed per slice before termination, regardless of convergence.
23
+ Defaults to 1000000.
24
+
25
+ Returns
26
+ -------
27
+ np.ndarray
28
+ A 3D numpy array containing the steady-state solution `u` for each 2D slice in `array_laplacian`.
29
+
30
+ Notes
31
+ -----
32
+ - The SOR method updates each element in the `u` array by considering its neighbors and applying the
33
+ relaxation factor `omega_SOR`.
34
+ - Boundaries are fixed to zero for each slice, enforcing Dirichlet boundary conditions.
35
+ - Convergence for each slice stops either when `delta` is less than `e` or after a stable count of
36
+ iterations (determined by `tolerance` and `max_stable_iters`) has been reached.
37
+
38
+ Examples
39
+ --------
40
+ >>> laplacian = np.random.rand(10, 100, 100) # 10 slices of 100x100 grids
41
+ >>> solution = SOR_2D(laplacian, omega_SOR=1.5, e=1e-6)
42
+ >>> print(solution.shape)
43
+ (10, 100, 100)
44
+ """
45
+ Lx=array_laplacian.shape[0]
46
+ Ly=array_laplacian.shape[1]
47
+ Lz=array_laplacian.shape[2]
48
+ delta = 1.0
49
+ n_iter=0
50
+ u_list=[]
51
+ stable_count = 0 # Reset stable count for each batch
52
+ prev_delta = float('inf') # Initialize previous delta
53
+ for slice_laplacian in tqdm(array_laplacian,desc="slice",leave=True):
54
+ u=np.zeros([Ly,Lz])
55
+ delta = 1.0
56
+ n_iter=0
57
+ while delta > e and stable_count < max_stable_iters:
58
+ u_in=u.copy()
59
+ delta = np.max(abs(u-u_in))
60
+ n_iter+=1
61
+ # Perform SOR update on the inner region
62
+ u[1:-1, 1:-1] = u[1:-1, 1:-1] + omega_SOR * (
63
+ (u_in[2:, 1:-1] + u_in[ :-2, 1:-1] + u_in[1:-1, 2:] + u_in[1:-1, :-2]
64
+ + slice_laplacian[1:-1, 1:-1]) / 4 - u[1:-1, 1:-1]
65
+ )
66
+
67
+ u[0][:]=0
68
+ u[Ly-1][:]=0
69
+ u[:][0]=0
70
+ u[:][Lz-1]=0
71
+
72
+ delta=np.max(abs(u-u_in))
73
+ # Check if residual change is within tolerance to count as stable
74
+ if abs(delta - prev_delta) < tolerance:
75
+ stable_count += 1
76
+ else:
77
+ stable_count = 0
78
+
79
+ prev_delta = delta # Update previous delta for next iteration
80
+
81
+ # Print iteration information
82
+ print("\r", f'Iteration: {n_iter}, Residual: {delta} Stable Count: {stable_count}', end="")
83
+
84
+ # Update iteration count
85
+ n_iter += 1
86
+
87
+ u_list.append(u)
88
+
89
+ return np.array(u_list)
BOSlib/evaluation.py ADDED
@@ -0,0 +1,344 @@
1
+ import open3d as o3d
2
+ import numpy as np
3
+ from tqdm import tqdm
4
+ import matplotlib.pyplot as plt
5
+ from matplotlib.colors import Normalize
6
+ import matplotlib.colors as mcolors
7
+ import statistics
8
+ import mpl_toolkits.axes_grid1
9
+
10
+ class CalculateDiff:
11
+ """
12
+ A class to calculate the difference between two point clouds in terms of neighbor densities.
13
+
14
+ Attributes
15
+ ----------
16
+ output_path : str
17
+ Path where the resulting point cloud with differences will be saved.
18
+ r : float
19
+ Radius for neighbor point sampling in KDTree.
20
+
21
+ Methods
22
+ -------
23
+ diff(pcl1, pcl2)
24
+ Calculates and visualizes the relative density differences between two point clouds.
25
+ ectraction_neighborpoints(pointcloud, target_positions)
26
+ Extracts the density of neighboring points around specified target positions.
27
+ """
28
+ def __init__(self, output_path: str, r: float) -> None:
29
+ """
30
+ Initializes the CalculateDiff class.
31
+
32
+ Parameters
33
+ ----------
34
+ output_path : str
35
+ Path to save the output point cloud.
36
+ r : float
37
+ Radius for neighbor point sampling in KDTree.
38
+ """
39
+ self.outputpath = output_path
40
+ self.r = r
41
+
42
+ def diff(self, pcl1, pcl2):
43
+ """
44
+ Calculates the relative density difference between two point clouds.
45
+
46
+ The method computes the difference in neighbor densities for points in two point clouds.
47
+ It normalizes the differences and clips them for visualization, then creates a new point
48
+ cloud to save the results.
49
+
50
+ Parameters
51
+ ----------
52
+ pcl1 : open3d.geometry.PointCloud
53
+ The first point cloud.
54
+ pcl2 : open3d.geometry.PointCloud
55
+ The second point cloud.
56
+
57
+ Returns
58
+ -------
59
+ open3d.geometry.PointCloud
60
+ The point cloud representing the relative density differences.
61
+ """
62
+ # Initialize the output point cloud
63
+ diff_pointcloud = o3d.geometry.PointCloud()
64
+
65
+ # Extract point positions from the input point clouds
66
+ positions_pcl1 = np.array(pcl1.points)
67
+ positions_pcl2 = np.array(pcl2.points)
68
+
69
+ # Use the sparser point cloud for density calculation
70
+ if positions_pcl1.shape[0] < positions_pcl2.shape[0]:
71
+ ground_position = positions_pcl1
72
+ else:
73
+ ground_position = positions_pcl2
74
+
75
+ # Compute neighbor densities for each point cloud
76
+ density_pcl1 = self.ectraction_neighborpoints(pcl1, ground_position)
77
+ density_pcl2 = self.ectraction_neighborpoints(pcl2, ground_position)
78
+ density_diff = density_pcl1 - density_pcl2
79
+
80
+ # Convert to relative error
81
+ density_diff_relative = 100 * np.divide(
82
+ np.abs(density_diff),
83
+ np.array(density_pcl1)
84
+ )
85
+
86
+ # Clip relative differences to a maximum of 100
87
+ density_diff_relative = np.clip(density_diff_relative, 0, 100)
88
+
89
+ # Apply the differences to the output point cloud
90
+ diff_pointcloud.normals = o3d.utility.Vector3dVector(density_diff_relative)
91
+ diff_pointcloud.points = o3d.utility.Vector3dVector(ground_position)
92
+
93
+ # Normalize density differences and map them to RGB values
94
+ RGB, minval, maxval = _normalize(density_diff)
95
+ diff_pointcloud.colors = o3d.utility.Vector3dVector(np.array(RGB))
96
+
97
+ # Save the resulting point cloud
98
+ o3d.io.write_point_cloud(self.outputpath, diff_pointcloud, format='pts', compressed=True)
99
+
100
+ return diff_pointcloud
101
+
102
+ def ectraction_neighborpoints(self, pointcloud, target_positions):
103
+ """
104
+ Extracts the density of neighbor points for given target positions in a point cloud.
105
+
106
+ This function uses KDTree for efficient neighbor search.
107
+
108
+ Parameters
109
+ ----------
110
+ pointcloud : open3d.geometry.PointCloud
111
+ The input point cloud.
112
+ target_positions : numpy.ndarray
113
+ Array of positions to search for neighbors.
114
+
115
+ Returns
116
+ -------
117
+ numpy.ndarray
118
+ Array of densities (number of neighbor points) for each target position.
119
+ """
120
+ # Create a KDTree for neighbor point search
121
+ kdtree = o3d.geometry.KDTreeFlann(pointcloud)
122
+ radius = self.r # Radius for neighbor search
123
+
124
+ all_indices = [] # List to store indices of neighbors
125
+ for pos in tqdm(target_positions, desc="Extracting neighbor points"):
126
+ [k, idx, _] = kdtree.search_radius_vector_3d(pos, radius)
127
+ if np.asarray(idx).shape[0] == 0:
128
+ index = [0]
129
+ elif np.asarray(idx).shape[0] == 1:
130
+ index = idx
131
+ else:
132
+ index = [np.asarray(idx)[0]]
133
+ all_indices.extend([index])
134
+
135
+ # Extract neighbor densities
136
+ neighbor_density = np.asarray(pointcloud.normals)[all_indices, :][:, 0]
137
+ neighbor_density_array = np.asarray(neighbor_density)
138
+ return neighbor_density_array
139
+
140
+ def _normalize(self,data):
141
+ """
142
+ Min-Maxスケーリングを使用してデータを正規化します。
143
+ """
144
+ min_val = np.min(data)
145
+ max_val = np.max(data)
146
+ normalized_data = [(x - min_val) / (max_val - min_val) for x in data]
147
+ return normalized_data, min_val, max_val
148
+
149
+ def viewer(
150
+ pointcloud_path: str, vmax: float, vcentre: float, vmin: float,
151
+ color: str, unit_colorbar: str, unit_xy: str, rho: float
152
+ ) -> None:
153
+ """
154
+ Visualizes a point cloud with color-coded density values as a scatter plot.
155
+
156
+ Parameters
157
+ ----------
158
+ pointcloud_path : str
159
+ Path to the point cloud file to be loaded.
160
+ vmax : float
161
+ Maximum value for the color scale. If None, it is set to the maximum of the normalized density.
162
+ vcentre : float
163
+ Center value for the color scale.
164
+ vmin : float
165
+ Minimum value for the color scale.
166
+ color : str
167
+ Colormap to use for visualization.
168
+ unit_colorbar : str
169
+ Label for the colorbar indicating the unit of the density values.
170
+ unit_xy : str
171
+ Label for the x and y axes indicating the unit of the coordinates.
172
+ rho : float
173
+ Normalization factor for density values.
174
+
175
+ Returns
176
+ -------
177
+ None
178
+ Displays a scatter plot of the point cloud with density visualized as colors.
179
+
180
+ Notes
181
+ -----
182
+ The density values are normalized by `rho`, and their statistics (max, min, mean, median)
183
+ are printed to the console. The point cloud's x and y coordinates are used for the scatter plot.
184
+ """
185
+ # Load the point cloud
186
+ pointcloud = o3d.io.read_point_cloud(pointcloud_path)
187
+
188
+ # Extract coordinates and density values
189
+ x = np.asarray(pointcloud.points)[:, 0]
190
+ y = np.asarray(pointcloud.points)[:, 1]
191
+ density = np.asarray(pointcloud.normals)[:, 0]
192
+ density_nondim = density / rho # Normalize density by rho
193
+
194
+ # Configure color normalization for the scatter plot
195
+ if vmax is None:
196
+ norm = Normalize(vmin=density_nondim.min(), vmax=density_nondim.max())
197
+ else:
198
+ # Use a TwoSlopeNorm for customized vmin, vcenter, and vmax
199
+ norm = mcolors.TwoSlopeNorm(vmin=vmin, vcenter=vcentre, vmax=vmax)
200
+
201
+ # Create figure and axes for the scatter plot
202
+ fig = plt.figure(figsize=(9, 6))
203
+ ax = fig.add_subplot(111)
204
+
205
+ # Plot the scatter plot
206
+ sc = ax.scatter(x, y, c=density_nondim, cmap=color, s=1, norm=norm)
207
+ ax.set_aspect('equal', adjustable='box') # Set equal aspect ratio
208
+
209
+ # Add a colorbar to the plot
210
+ divider = mpl_toolkits.axes_grid1.make_axes_locatable(ax)
211
+ cax = divider.append_axes('right', '5%', pad=0.1)
212
+ cbar = plt.colorbar(sc, ax=ax, cax=cax, orientation='vertical')
213
+ cbar.set_label(unit_colorbar) # Set colorbar label
214
+
215
+ # Set axis labels and titles
216
+ ax.set_xlabel(unit_xy)
217
+ ax.set_ylabel(unit_xy)
218
+
219
+ # Display the plot
220
+ plt.show()
221
+
222
+ class array2pointcloud:
223
+ """
224
+ Converts a 2D array into a 3D point cloud with associated density and color information.
225
+
226
+ Parameters
227
+ ----------
228
+ px2mm_y : float
229
+ Conversion factor from pixels to millimeters along the y-axis.
230
+ px2mm_x : float
231
+ Conversion factor from pixels to millimeters along the x-axis.
232
+ array : np.array
233
+ Input 2D array representing pixel data.
234
+ ox : int
235
+ Origin offset in pixels along the x-axis.
236
+ oy : int
237
+ Origin offset in pixels along the y-axis.
238
+ outpath : str
239
+ Path where the resulting point cloud file will be saved.
240
+ Flip : bool
241
+ Whether to flip the array horizontally.
242
+
243
+ Attributes
244
+ ----------
245
+ data_px : np.array
246
+ The original or flipped array data in pixel units.
247
+ data_mm : np.array
248
+ Transformed data with coordinates in millimeter units and density as the fourth column.
249
+ points : np.array
250
+ 3D points representing the x, y, and z coordinates.
251
+ density : np.array
252
+ Density values expanded for storing as normals.
253
+ RGB : np.array
254
+ RGB color values derived from the density data.
255
+
256
+ Methods
257
+ -------
258
+ __call__()
259
+ Executes the conversion process and saves the resulting point cloud.
260
+ px2mm_method()
261
+ Converts the pixel coordinates and density values to millimeter units.
262
+ reshaper()
263
+ Extracts and reshapes the 3D points, density, and RGB values.
264
+ set_array()
265
+ Assembles the data into an Open3D PointCloud object.
266
+ """
267
+ def __init__(self, px2mm_y: float, px2mm_x: float, array: np.array,
268
+ ox: int, oy: int, outpath: str, Flip: bool) -> None:
269
+ self.px2mm_x = px2mm_x
270
+ self.px2mm_y = px2mm_y
271
+ self.data_px = array
272
+ self.ox = ox
273
+ self.oy = oy
274
+ self.output_path = outpath
275
+ self.flip = Flip
276
+
277
+ # Initialize placeholders for processed data
278
+ self.data_mm = None
279
+ self.points = None
280
+ self.density = None
281
+ self.RGB = None
282
+
283
+ def __call__(self):
284
+ """
285
+ Executes the full conversion pipeline and saves the result as a point cloud.
286
+ """
287
+ self.px2mm_method()
288
+ self.reshaper()
289
+ pcd = self.set_array()
290
+ o3d.io.write_point_cloud(self.output_path, pcd, format='pts', compressed=True)
291
+
292
+ def px2mm_method(self):
293
+ """
294
+ Converts pixel-based coordinates to millimeter-based coordinates.
295
+
296
+ Notes
297
+ -----
298
+ If `self.flip` is True, the input array is flipped horizontally. The resulting
299
+ millimeter-based coordinates and density values are stored in `self.data_mm`.
300
+ """
301
+ if self.flip:
302
+ self.data_px = np.fliplr(self.data_px)
303
+
304
+ data_list = []
305
+ for i in range(self.data_px.shape[0]):
306
+ for j in range(self.data_px.shape[1]):
307
+ # Calculate millimeter coordinates and append density value
308
+ point = [float(self.px2mm_x * (j - self.ox)),
309
+ float(self.px2mm_y * (i - self.oy)),
310
+ 0.0, # z-coordinate is 0
311
+ float(self.data_px[i, j])]
312
+ data_list.append(point)
313
+
314
+ self.data_mm = np.array(data_list)
315
+
316
+ def reshaper(self):
317
+ """
318
+ Reshapes the transformed data into points, density, and RGB values.
319
+
320
+ Notes
321
+ -----
322
+ Density values are tiled to create normals for the point cloud.
323
+ The `nm` function is used to normalize density values into RGB colors.
324
+ """
325
+ self.points = self.data_mm[:, :3] # Extract 3D coordinates
326
+ self.density = np.tile(self.data_mm[:, 3], (3, 1)).T # Expand density values
327
+ colors, _, _ = _normalize(self.density) # Normalize density to RGB
328
+ self.RGB = np.array(colors)
329
+
330
+ def set_array(self):
331
+ """
332
+ Creates an Open3D PointCloud object from the processed data.
333
+
334
+ Returns
335
+ -------
336
+ pcd : o3d.geometry.PointCloud
337
+ The resulting point cloud object with points, colors, and normals.
338
+ """
339
+ pcd = o3d.geometry.PointCloud()
340
+ pcd.points = o3d.utility.Vector3dVector(self.points)
341
+ pcd.colors = o3d.utility.Vector3dVector(self.RGB)
342
+ pcd.normals = o3d.utility.Vector3dVector(self.density)
343
+
344
+ return pcd
@@ -0,0 +1,167 @@
1
+ import numpy as np
2
+ from tqdm import tqdm
3
+ from tqdm.contrib import tzip
4
+ from skimage.transform import radon, iradon
5
+
6
+ def abel_transform(angle: np.ndarray, center: float, winy0: int, winy1: int, winx0: int, winx1: int) -> np.ndarray:
7
+ """
8
+ Perform the Abel transform to convert refractive angle values into density differences.
9
+
10
+ This function applies the Abel transform on a 2D array of refractive angles. It compensates
11
+ for background movement by subtracting the mean value at a reference x-coordinate, calculates
12
+ distances from the center axis, and integrates to derive density differences using the
13
+ Gladstone-Dale constant.
14
+
15
+ Parameters
16
+ ----------
17
+ angle : np.ndarray
18
+ A 2D numpy array representing refractive angles for each pixel.
19
+ center : float
20
+ The index along the y-axis corresponding to the central axis of the transform.
21
+ winy0 : int
22
+ The starting index along the y-axis for the region used to calculate the background mean.
23
+ winy1 : int
24
+ The ending index along the y-axis for the region used to calculate the background mean.
25
+ winx0 : int
26
+ The starting index along the x-axis for the region used to calculate the background mean.
27
+ winx1 : int
28
+ The ending index along the x-axis for the region used to calculate the background mean.
29
+
30
+ Returns
31
+ -------
32
+ np.ndarray
33
+ A 2D array of refractive index differences derived from the Abel transform.
34
+
35
+ Notes
36
+ -----
37
+ This function calculates density differences through an integral-based approach. The refractive
38
+ angle image is rotated to align with the axis of symmetry, and values are integrated from the
39
+ center outwards, adjusting for axial symmetry.
40
+ """
41
+
42
+ # Offset the angle values by subtracting the mean value at the reference x-coordinate
43
+ angle = angle - np.mean(angle[winy0:winy1,winx0:winx1])
44
+
45
+ # Remove values below the center since they are not used in the calculation
46
+ angle = angle[0:center]
47
+
48
+ # Reverse the angle array so that the upper end becomes the central axis
49
+ angle = angle[::-1]
50
+
51
+ # Calculate the distance from the central axis (η)
52
+ eta = np.array(range(angle.shape[0]))
53
+
54
+ # Initialize an array to store the results
55
+ ans = np.zeros_like(angle)
56
+
57
+ # Calculate the values outward from r=0
58
+ for r in tqdm(range(center)):
59
+ # A: Denominator √(η² - r²)
60
+ # Calculate η² - r²
61
+ A = eta**2 - r**2
62
+ # Trim the array to keep the integration range (we extend to r+1 to avoid division by zero)
63
+ A = A[r+1:center]
64
+ # Take the square root to obtain √(η² - r²)
65
+ A = np.sqrt(A)
66
+ # Reshape for broadcasting
67
+ A = np.array([A]).T
68
+
69
+ # B: The integrand (1/π * ε/√(η² - r²))
70
+ B = angle[r+1:center] / (A * np.pi)
71
+ # Sum B vertically to perform integration
72
+ ans[r] = B.sum(axis=0)
73
+
74
+
75
+
76
+ return ans
77
+
78
+ def ART(sinogram, mu, e, reconstruction_angle : float,circle=True):
79
+ """
80
+ Perform Algebraic Reconstruction Technique (ART) to reconstruct images from a sinogram.
81
+
82
+ The ART method iteratively updates pixel values to minimize the error between projections
83
+ and the input sinogram, facilitating accurate image reconstruction from projections.
84
+
85
+ Parameters
86
+ ----------
87
+ sinogram : np.ndarray
88
+ A 2D or 3D numpy array representing the sinogram. Each row corresponds to a projection
89
+ at a specific angle.
90
+ mu : float
91
+ The relaxation parameter controlling the update step size during the iterative process.
92
+ e : float
93
+ The convergence threshold for the maximum absolute error in the reconstruction.
94
+
95
+ Returns
96
+ -------
97
+ list of np.ndarray
98
+ A list of reconstructed 2D arrays, each corresponding to a projection set in the input sinogram.
99
+
100
+ Notes
101
+ -----
102
+ - The function dynamically adjusts the grid size `N` until it matches the shape of the sinogram projections.
103
+ - The `radon` and `iradon` functions from `skimage.transform` are used to perform forward and backward
104
+ projections, respectively.
105
+ - The method stops when the maximum absolute error between successive updates falls below `e`.
106
+
107
+ Examples
108
+ --------
109
+ >>> sinogram = np.random.rand(180, 128) # Example sinogram with 180 projections of length 128
110
+ >>> reconstructed_images = ART(sinogram, mu=0.1, e=1e-6)
111
+ >>> print(len(reconstructed_images))
112
+ 180
113
+ """
114
+
115
+ N = 1 # Initial grid size for reconstruction
116
+ ANG = reconstruction_angle # Total rotation angle for projections
117
+ VIEW = sinogram[0].shape[0] # Number of views (angles) in each projection
118
+ THETA = np.linspace(0, ANG, VIEW + 1)[:-1] # Angles for radon transform
119
+ pbar = tqdm(total=sinogram[0].shape[0], desc="Initialization", unit="task")
120
+
121
+ # Find the optimal N that matches the projection dimensions
122
+ while True:
123
+ x = np.ones((N, N)) # Initialize a reconstruction image with ones
124
+
125
+ def A(x):
126
+ # Forward projection (Radon transform)
127
+ return radon(x, THETA, circle=circle).astype(np.float32)
128
+
129
+ def AT(y):
130
+ # Backprojection (inverse Radon transform)
131
+ return iradon(y, THETA, circle=circle, output_size=N).astype(np.float32) / (np.pi/2 * len(THETA))
132
+
133
+ ATA = AT(A(np.ones_like(x))) # ATA matrix for scaling
134
+
135
+ # Check if the current grid size N produces projections of the correct shape
136
+ if A(x).shape[0] == sinogram[0].shape[0]:
137
+ break
138
+
139
+ # Adjust N in larger steps if the difference is significant, else by 1
140
+ if sinogram[0].shape[0] - A(x).shape[0] > 20:
141
+ N += 10
142
+ else:
143
+ N += 1
144
+
145
+ # Update progress bar
146
+ pbar.n = A(x).shape[0]
147
+ pbar.refresh()
148
+ pbar.close()
149
+
150
+ loss = np.inf
151
+ x_list = []
152
+
153
+ # Process each projection set in the sinogram
154
+ for i in tqdm(range(sinogram.shape[0]), desc='Process', leave=True):
155
+ b = sinogram[i] # Current projection data
156
+ ATA = AT(A(np.ones_like(x))) # Recalculate ATA for current x
157
+ loss = float('inf') # Reset loss
158
+
159
+ # Iteratively update x until convergence
160
+ while np.max(np.abs(loss)) > e:
161
+ # Compute the update based on the difference between projection and reconstruction
162
+ loss = np.divide(AT(b - A(x)), ATA)
163
+ x = x + mu * loss
164
+
165
+ x_list.append(x) # Append the reconstructed image for the current projection
166
+
167
+ return x_list
@@ -0,0 +1,47 @@
1
+ from tqdm import tqdm
2
+ import numpy as np
3
+
4
+ def sinogram_maker_axialsymmetry(angle):
5
+ """
6
+ Generates a sinogram with axial symmetry from a single 2D refractive angle image.
7
+
8
+ Parameters
9
+ ----------
10
+ angle : np.ndarray
11
+ A 2D numpy array representing the refractive angle image. Each row in this array
12
+ is broadcast across the height dimension to simulate an axially symmetric sinogram.
13
+
14
+ Returns
15
+ -------
16
+ np.ndarray
17
+ A 3D numpy array representing the sinogram with axial symmetry. Each slice along
18
+ the first dimension corresponds to a projection at a different angle, where each
19
+ projection is a symmetric repetition of the refractive angle row values across
20
+ the height and width dimensions.
21
+
22
+ Notes
23
+ -----
24
+ This function assumes axial symmetry for the generated sinogram by replicating each row
25
+ of the input angle image across both dimensions (height and width) for each slice in
26
+ the 3D sinogram. The input image is first rotated by 90 degrees for alignment.
27
+
28
+ Examples
29
+ --------
30
+ >>> angle_image = np.random.rand(100, 200) # A 100x200 refractive angle image
31
+ >>> sinogram = sinogram_maker_axialsymmetry(angle_image)
32
+ >>> print(sinogram.shape)
33
+ (200, 200, 200)
34
+ """
35
+ # Rotate the angle image by 90 degrees
36
+ angle = np.rot90(angle)
37
+ height = angle.shape[1]
38
+
39
+ # Initialize an empty 3D array for the sinogram
40
+ sinogram = np.empty((angle.shape[0], height, height), dtype=angle.dtype)
41
+
42
+ # Loop through each row in the rotated angle image
43
+ for i, d_angle in enumerate(tqdm(angle)):
44
+ # Broadcast each row across the height to create a symmetric 2D projection
45
+ sinogram[i] = np.broadcast_to(d_angle[:, np.newaxis], (height, height))
46
+
47
+ return sinogram