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 +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
|