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 +9 -0
- BOSlib/culculate_refractiveindex.py +89 -0
- BOSlib/evaluation.py +344 -0
- BOSlib/reconstruction.py +167 -0
- BOSlib/reconstruction_utils.py +47 -0
- BOSlib/shift.py +126 -0
- BOSlib/shift_utils.py +306 -0
- BOSlib/utils.py +204 -0
- BOSlib-0.0.1.dist-info/LICENSE +674 -0
- BOSlib-0.0.1.dist-info/METADATA +84 -0
- BOSlib-0.0.1.dist-info/RECORD +13 -0
- BOSlib-0.0.1.dist-info/WHEEL +5 -0
- BOSlib-0.0.1.dist-info/top_level.txt +1 -0
BOSlib/__init__.py
ADDED
@@ -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
|
BOSlib/reconstruction.py
ADDED
@@ -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
|