BOSlib 0.0.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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