BOSlib 0.0.1__tar.gz → 0.0.3__tar.gz
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-0.0.1 → boslib-0.0.3}/BOSlib/__init__.py +1 -1
- boslib-0.0.3/BOSlib/evaluation.py +344 -0
- boslib-0.0.3/BOSlib/shift.py +351 -0
- {boslib-0.0.1 → boslib-0.0.3}/BOSlib.egg-info/PKG-INFO +1 -1
- {boslib-0.0.1 → boslib-0.0.3}/PKG-INFO +1 -1
- boslib-0.0.1/BOSlib/evaluation.py +0 -344
- boslib-0.0.1/BOSlib/shift.py +0 -126
- {boslib-0.0.1 → boslib-0.0.3}/BOSlib/culculate_refractiveindex.py +0 -0
- {boslib-0.0.1 → boslib-0.0.3}/BOSlib/reconstruction.py +0 -0
- {boslib-0.0.1 → boslib-0.0.3}/BOSlib/reconstruction_utils.py +0 -0
- {boslib-0.0.1 → boslib-0.0.3}/BOSlib/shift_utils.py +0 -0
- {boslib-0.0.1 → boslib-0.0.3}/BOSlib/utils.py +0 -0
- {boslib-0.0.1 → boslib-0.0.3}/BOSlib.egg-info/SOURCES.txt +0 -0
- {boslib-0.0.1 → boslib-0.0.3}/BOSlib.egg-info/dependency_links.txt +0 -0
- {boslib-0.0.1 → boslib-0.0.3}/BOSlib.egg-info/requires.txt +0 -0
- {boslib-0.0.1 → boslib-0.0.3}/BOSlib.egg-info/top_level.txt +0 -0
- {boslib-0.0.1 → boslib-0.0.3}/LICENSE +0 -0
- {boslib-0.0.1 → boslib-0.0.3}/README.md +0 -0
- {boslib-0.0.1 → boslib-0.0.3}/setup.cfg +0 -0
- {boslib-0.0.1 → boslib-0.0.3}/setup.py +0 -0
@@ -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(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,351 @@
|
|
1
|
+
from skimage.metrics import structural_similarity as ssm
|
2
|
+
import numpy as np
|
3
|
+
from PIL import Image
|
4
|
+
from scipy import signal
|
5
|
+
import pandas as pd
|
6
|
+
|
7
|
+
import BOSlib.shift_utils as ib
|
8
|
+
|
9
|
+
|
10
|
+
def SSIM(ref_array : np.ndarray, exp_array : np.ndarray):
|
11
|
+
"""
|
12
|
+
Compute the inverted Structural Similarity Index (SSIM) difference matrix between two grayscale images.
|
13
|
+
|
14
|
+
Parameters
|
15
|
+
----------
|
16
|
+
ref_array : np.ndarray
|
17
|
+
The reference grayscale image array.
|
18
|
+
exp_array : np.ndarray
|
19
|
+
The experimental grayscale image array.
|
20
|
+
|
21
|
+
Returns
|
22
|
+
-------
|
23
|
+
np.ndarray
|
24
|
+
The inverted SSIM difference matrix, where higher values indicate greater dissimilarity between the two images.
|
25
|
+
"""
|
26
|
+
# Compute the structural similarity matrix (SSM) on the grayscale images
|
27
|
+
(score, diff) = ssm(ref_array, exp_array, full=True)
|
28
|
+
diff_inv = -diff
|
29
|
+
return diff_inv
|
30
|
+
|
31
|
+
def SP_BOS(ref_array : np.ndarray, exp_array : np.ndarray, binarization : str ="HPfilter", thresh : int = 128, freq : int = 500):
|
32
|
+
"""
|
33
|
+
Calculate the displacement map of stripe patterns in experimental images using the Background Oriented Schlieren (BOS) method.
|
34
|
+
|
35
|
+
This function computes the relative displacement between stripes in a reference and experimental image by compensating for background movement and noise. The displacement map is calculated by processing the images through several steps including image resizing, binarization, boundary detection, noise reduction, displacement calculation, and background compensation.
|
36
|
+
|
37
|
+
Parameters
|
38
|
+
----------
|
39
|
+
ref_array : np.ndarray
|
40
|
+
The reference grayscale image array. This image represents the original, undisturbed pattern.
|
41
|
+
|
42
|
+
exp_array : np.ndarray
|
43
|
+
The experimental grayscale image array. This image represents the pattern after deformation due to external factors.
|
44
|
+
|
45
|
+
binarization : str, optional, default="HPfilter"
|
46
|
+
The method used for binarization of the images. Options are:
|
47
|
+
- "thresh" : Use thresholding for binarization.
|
48
|
+
- "HPfilter" : Use high-pass filtering for binarization.
|
49
|
+
|
50
|
+
thresh : int, optional, default=128
|
51
|
+
The threshold value used for binarization when `binarization="thresh"`. Pixels with values above the threshold are set to 1, and those below are set to 0.
|
52
|
+
|
53
|
+
freq : int, optional, default=500
|
54
|
+
The frequency parameter used for high-pass filtering when `binarization="HPfilter"`.
|
55
|
+
|
56
|
+
Returns
|
57
|
+
-------
|
58
|
+
np.ndarray
|
59
|
+
A 2D array representing the displacement map of the stripe patterns, with background movement compensated. Each value represents the relative displacement between the reference and experimental images, with noise and background displacements removed.
|
60
|
+
|
61
|
+
Notes
|
62
|
+
-----
|
63
|
+
The method performs the following steps:
|
64
|
+
1. Vertically stretches both the reference and experimental images by a factor of 10.
|
65
|
+
2. Binarizes the images using either thresholding or high-pass filtering.
|
66
|
+
3. Identifies the upper and lower boundaries of the stripes and calculates their centers for both images.
|
67
|
+
4. Filters out noise by removing displacements larger than a certain threshold.
|
68
|
+
5. Computes the displacement between the stripe centers.
|
69
|
+
6. Compensates for background movement by normalizing the displacement map, subtracting the mean displacement over a specified region.
|
70
|
+
"""
|
71
|
+
|
72
|
+
im_ref=Image.fromarray(ref_array)
|
73
|
+
im_exp=Image.fromarray(exp_array)
|
74
|
+
|
75
|
+
#streach the image vertivally *10
|
76
|
+
im_ref=im_ref.resize((im_ref.size[0],im_ref.size[1]*10))
|
77
|
+
im_exp=im_exp.resize((im_exp.size[0],im_exp.size[1]*10))
|
78
|
+
|
79
|
+
ar_ref=np.array(im_ref)
|
80
|
+
ar_exp=np.array(im_exp)
|
81
|
+
|
82
|
+
if binarization =="thresh":
|
83
|
+
# Binarization
|
84
|
+
bin_ref = ib._biner_thresh(ar_ref, thresh)
|
85
|
+
bin_exp = ib._biner_thresh(ar_exp, thresh)
|
86
|
+
|
87
|
+
#print("Binarization",bin_ref.shape,bin_exp.shape)
|
88
|
+
elif binarization =="HPfilter":
|
89
|
+
bin_ref=ib._biner_HP(ar_ref, freq)
|
90
|
+
bin_exp=ib._biner_HP(ar_exp, freq)
|
91
|
+
#print("Binarization",bin_ref.shape,bin_exp.shape)
|
92
|
+
else:
|
93
|
+
raise ValueError("Binarization is thresh or HPfilter")
|
94
|
+
|
95
|
+
# Detect the coordinates of the color boundaries in the binarized reference image
|
96
|
+
ref_u, ref_d = ib._bin_indexer(bin_ref)
|
97
|
+
ref_u = np.nan_to_num(ref_u)
|
98
|
+
ref_d = np.nan_to_num(ref_d)
|
99
|
+
#print("bin_indexer_ref",ref_u.shape,ref_d.shape)
|
100
|
+
# Detect the coordinates of the color boundaries in the binarized experimental image
|
101
|
+
# u represents the upper boundary of the white stripe, d represents the lower boundary
|
102
|
+
exp_u, exp_d = ib._bin_indexer(bin_exp)
|
103
|
+
exp_u = np.nan_to_num(exp_u)
|
104
|
+
exp_d = np.nan_to_num(exp_d)
|
105
|
+
#print("bin_indexer_exp",exp_u.shape,exp_d.shape)
|
106
|
+
|
107
|
+
# Remove data with abnormally large displacements as noise
|
108
|
+
ref_u, exp_u = ib._noize_reducer_2(ref_u, exp_u, 10)
|
109
|
+
ref_d, exp_d = ib._noize_reducer_2(ref_d, exp_d, 10)
|
110
|
+
#print("noize_reducer_2",exp_u.shape,exp_d.shape)
|
111
|
+
#print("noize_reducer_2",ref_u.shape,ref_d.shape)
|
112
|
+
|
113
|
+
# Combine the upper and lower boundary data to calculate the center of the stripe
|
114
|
+
ref = ib._mixing(ref_u, ref_d)
|
115
|
+
exp = ib._mixing(exp_u, exp_d)
|
116
|
+
|
117
|
+
#print("mixing",ref.shape,exp.shape)
|
118
|
+
|
119
|
+
# Calculate displacement (upward displacement is positive)
|
120
|
+
diff = -(exp - ref)
|
121
|
+
|
122
|
+
# Rearrange the displacement values into the correct positions and interpolate gaps
|
123
|
+
diff_comp = ib._complementer(ref, diff)
|
124
|
+
|
125
|
+
#print("complementer",diff_comp.shape)
|
126
|
+
|
127
|
+
# Subtract the overall background movement by dividing by the mean displacement
|
128
|
+
diff_comp = diff_comp - np.nanmean(diff_comp[0:1000, 10:100])
|
129
|
+
|
130
|
+
return diff_comp
|
131
|
+
|
132
|
+
def S_BOS(ref_array: np.ndarray, exp_array: np.ndarray,freq_sample_area):
|
133
|
+
"""
|
134
|
+
Compute the phase difference and corresponding displacement (delta_h)
|
135
|
+
between reference and experimental signal arrays using a 1D Background
|
136
|
+
Oriented Schlieren (BOS) process.
|
137
|
+
|
138
|
+
Parameters:
|
139
|
+
-----------
|
140
|
+
ref_array : np.ndarray
|
141
|
+
2D numpy array containing reference signals, where each column is a distinct signal.
|
142
|
+
exp_array : np.ndarray
|
143
|
+
2D numpy array containing experimental signals, matching the dimensions of ref_array.
|
144
|
+
|
145
|
+
Returns:
|
146
|
+
--------
|
147
|
+
delta_h : np.ndarray
|
148
|
+
2D numpy array representing the displacement computed from the phase differences.
|
149
|
+
"""
|
150
|
+
|
151
|
+
def freq_finder(sig):
|
152
|
+
"""
|
153
|
+
Identify the dominant frequency in the signal using the FFT.
|
154
|
+
|
155
|
+
Parameters:
|
156
|
+
-----------
|
157
|
+
sig : np.ndarray
|
158
|
+
1D numpy array representing a signal.
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
--------
|
162
|
+
float
|
163
|
+
The dominant frequency (above 0.01 Hz) based on amplitude.
|
164
|
+
"""
|
165
|
+
# Compute FFT frequencies
|
166
|
+
freq = np.fft.fftfreq(sig.shape[0])
|
167
|
+
# Compute FFT of the signal and normalize the amplitude
|
168
|
+
fk = np.fft.fft(sig)
|
169
|
+
fk = abs(fk / (sig.shape[0] / 2))
|
170
|
+
# Combine frequencies and amplitudes into a DataFrame
|
171
|
+
fk_df = pd.DataFrame(np.vstack([freq, fk]).T, columns=["freq", "amp"])
|
172
|
+
# Sort DataFrame by frequency and keep only non-negative frequencies
|
173
|
+
fk_df = fk_df.sort_values('freq')
|
174
|
+
fk_df = fk_df[fk_df["freq"] >= 0]
|
175
|
+
# Select frequencies above 0.01 Hz and sort by amplitude in descending order
|
176
|
+
freq_search = fk_df[fk_df["freq"] >= 0.01].sort_values('amp', ascending=False)
|
177
|
+
# Return the frequency corresponding to the highest amplitude
|
178
|
+
return freq_search.iloc[0, 0]
|
179
|
+
|
180
|
+
def bandpass(x, fa, fb):
|
181
|
+
"""
|
182
|
+
Apply a bandpass Butterworth filter to the signal.
|
183
|
+
|
184
|
+
Parameters:
|
185
|
+
-----------
|
186
|
+
x : np.ndarray
|
187
|
+
Input signal.
|
188
|
+
fa : float
|
189
|
+
Lower cutoff frequency multiplier.
|
190
|
+
fb : float
|
191
|
+
Upper cutoff frequency multiplier.
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
--------
|
195
|
+
np.ndarray
|
196
|
+
The bandpass-filtered signal.
|
197
|
+
"""
|
198
|
+
gpass, gstop = 2, 60 # Passband and stopband gains (dB)
|
199
|
+
fp, fs = np.array([fa, fb]), np.array([fa / 2, fb * 2])
|
200
|
+
fn = 1 / 2 # Nyquist frequency (assuming a normalized sample rate)
|
201
|
+
wp, ws = fp / fn, fs / fn
|
202
|
+
# Determine the minimum filter order that meets the specifications
|
203
|
+
N, Wn = signal.buttord(wp, ws, gpass, gstop)
|
204
|
+
# Get the filter coefficients for a Butterworth filter
|
205
|
+
b, a = signal.butter(N, Wn, "band")
|
206
|
+
# Apply the filter forward and backward to avoid phase distortion
|
207
|
+
return signal.filtfilt(b, a, x)
|
208
|
+
|
209
|
+
def lowpass(x, lowcut):
|
210
|
+
"""
|
211
|
+
Apply a lowpass Butterworth filter to the signal.
|
212
|
+
|
213
|
+
Parameters:
|
214
|
+
-----------
|
215
|
+
x : np.ndarray
|
216
|
+
Input signal.
|
217
|
+
lowcut : float
|
218
|
+
The low cutoff frequency.
|
219
|
+
|
220
|
+
Returns:
|
221
|
+
--------
|
222
|
+
np.ndarray
|
223
|
+
The lowpass-filtered signal.
|
224
|
+
"""
|
225
|
+
order, nyq = 8, 0.5 * 1 # Order and Nyquist frequency (assuming sample rate = 1)
|
226
|
+
low = lowcut / nyq
|
227
|
+
# Get the filter coefficients for a lowpass Butterworth filter
|
228
|
+
b, a = signal.butter(order, low, btype='low')
|
229
|
+
# Apply the filter with zero-phase distortion
|
230
|
+
return signal.filtfilt(b, a, x)
|
231
|
+
|
232
|
+
def signal_separate(sig, f1):
|
233
|
+
"""
|
234
|
+
Separate the signal into a constant (mean) component and a bandpass-filtered component.
|
235
|
+
|
236
|
+
Parameters:
|
237
|
+
-----------
|
238
|
+
sig : np.ndarray
|
239
|
+
Input signal.
|
240
|
+
f1 : float
|
241
|
+
Base frequency used to define the bandpass range.
|
242
|
+
|
243
|
+
Returns:
|
244
|
+
--------
|
245
|
+
np.ndarray
|
246
|
+
2D array where the first column is the signal mean and the second column is the bandpass-filtered signal.
|
247
|
+
"""
|
248
|
+
sig_f = np.zeros([sig.shape[0], 2])
|
249
|
+
# First column: constant value equal to the mean of the signal
|
250
|
+
sig_f[:, 0] = sig.mean()
|
251
|
+
# Second column: bandpass filtered signal using a frequency window around f1
|
252
|
+
sig_f[:, 1] = bandpass(sig, f1 * 0.7, f1 * 1.5)
|
253
|
+
return sig_f
|
254
|
+
|
255
|
+
def signal_scale_normalize(sig, f):
|
256
|
+
"""
|
257
|
+
Normalize the signal based on a rolling maximum amplitude and add a sine correction.
|
258
|
+
|
259
|
+
Parameters:
|
260
|
+
-----------
|
261
|
+
sig : np.ndarray
|
262
|
+
Input signal.
|
263
|
+
f : float
|
264
|
+
Frequency used to calculate the sine correction.
|
265
|
+
|
266
|
+
Returns:
|
267
|
+
--------
|
268
|
+
np.ndarray
|
269
|
+
The normalized signal.
|
270
|
+
"""
|
271
|
+
# Calculate the rolling maximum absolute value over a window of 0.5/f samples
|
272
|
+
sig_abs = np.array(pd.Series(abs(sig)).rolling(int(0.5 / f), center=True).max())
|
273
|
+
# Suppress parts of the signal where the amplitude is significantly below the mean
|
274
|
+
sig[sig_abs < np.nanmean(sig_abs) * 0.5] = 0
|
275
|
+
y = np.arange(0, sig.shape[0], 1)
|
276
|
+
# Generate a sine wave for phase correction
|
277
|
+
S = np.sin(2 * np.pi * f * y)
|
278
|
+
# Create a correction term based on the amplitude threshold
|
279
|
+
S1 = (1 - (sig_abs > np.nanmean(sig_abs * 0.5))) * S
|
280
|
+
# Add the correction term to the signal
|
281
|
+
sig = sig + S1
|
282
|
+
# Avoid division by very small numbers
|
283
|
+
sig_abs[sig_abs < np.nanmean(sig_abs * 0.5)] = 1
|
284
|
+
# Normalize the signal
|
285
|
+
sig_norm = sig / sig_abs
|
286
|
+
sig_norm[np.isnan(sig_norm)] = 0
|
287
|
+
return sig_norm
|
288
|
+
|
289
|
+
def phase_calculate(ref, exp, f1):
|
290
|
+
"""
|
291
|
+
Calculate the phase difference between the reference and experimental signals.
|
292
|
+
|
293
|
+
Parameters:
|
294
|
+
-----------
|
295
|
+
ref : np.ndarray
|
296
|
+
Normalized reference signal.
|
297
|
+
exp : np.ndarray
|
298
|
+
Normalized experimental signal.
|
299
|
+
f1 : float
|
300
|
+
Base frequency.
|
301
|
+
|
302
|
+
Returns:
|
303
|
+
--------
|
304
|
+
np.ndarray
|
305
|
+
The phase difference calculated using lowpass filtered sine and cosine components.
|
306
|
+
"""
|
307
|
+
# Compute sine and its gradient (approximation for cosine)
|
308
|
+
sin_ref = ref
|
309
|
+
cos_ref = np.gradient(ref) / (f1 * 2 * np.pi)
|
310
|
+
# Compute lowpass filtered products to obtain sine and cosine components of the phase difference
|
311
|
+
cos_phi = lowpass(sin_ref * exp, f1)
|
312
|
+
sin_phi = lowpass(cos_ref * exp, f1)
|
313
|
+
# Calculate the phase difference using arctan2 for correct quadrant determination
|
314
|
+
return np.arctan2(sin_phi, cos_phi)
|
315
|
+
|
316
|
+
def phase_1DBOS_process(sig_ref, sig_exp, f1):
|
317
|
+
"""
|
318
|
+
Process a pair of reference and experimental signals to compute the phase difference.
|
319
|
+
|
320
|
+
Parameters:
|
321
|
+
-----------
|
322
|
+
sig_ref : np.ndarray
|
323
|
+
Single column from the reference signal array.
|
324
|
+
sig_exp : np.ndarray
|
325
|
+
Corresponding column from the experimental signal array.
|
326
|
+
f1 : float
|
327
|
+
Base frequency obtained from the reference array.
|
328
|
+
|
329
|
+
Returns:
|
330
|
+
--------
|
331
|
+
np.ndarray
|
332
|
+
The phase difference between the processed reference and experimental signals.
|
333
|
+
"""
|
334
|
+
# Separate the signal into mean and bandpass-filtered components and normalize them
|
335
|
+
separate_sig_ref = signal_scale_normalize(signal_separate(sig_ref, f1)[:, 1], f1)
|
336
|
+
separate_sig_exp = signal_scale_normalize(signal_separate(sig_exp, f1)[:, 1], f1)
|
337
|
+
# Calculate the phase difference between the normalized signals
|
338
|
+
return phase_calculate(separate_sig_ref, separate_sig_exp, f1)
|
339
|
+
|
340
|
+
# Determine the dominant frequency from a representative column (column 100) of the reference array
|
341
|
+
f1 = freq_finder(ref_array[freq_sample_area])
|
342
|
+
# Initialize a 2D array to store phase differences for each column
|
343
|
+
phi_2D = np.zeros([ref_array.shape[0], ref_array.shape[1]]).astype("float64")
|
344
|
+
|
345
|
+
# Process each column of the reference and experimental arrays
|
346
|
+
for x in range(ref_array.shape[1]):
|
347
|
+
phi_2D[:, x] = phase_1DBOS_process(ref_array[:, x], exp_array[:, x], f1)
|
348
|
+
|
349
|
+
# Convert phase differences into displacement by dividing by (2*pi*f1)
|
350
|
+
delta_h = phi_2D / (2 * np.pi * f1)
|
351
|
+
return delta_h
|
@@ -1,344 +0,0 @@
|
|
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-0.0.1/BOSlib/shift.py
DELETED
@@ -1,126 +0,0 @@
|
|
1
|
-
from skimage.metrics import structural_similarity as ssm
|
2
|
-
import numpy as np
|
3
|
-
from PIL import Image
|
4
|
-
import BOSlib.shift_utils as ib
|
5
|
-
|
6
|
-
def SSIM(ref_array : np.ndarray, exp_array : np.ndarray):
|
7
|
-
"""
|
8
|
-
Compute the inverted Structural Similarity Index (SSIM) difference matrix between two grayscale images.
|
9
|
-
|
10
|
-
Parameters
|
11
|
-
----------
|
12
|
-
ref_array : np.ndarray
|
13
|
-
The reference grayscale image array.
|
14
|
-
exp_array : np.ndarray
|
15
|
-
The experimental grayscale image array.
|
16
|
-
|
17
|
-
Returns
|
18
|
-
-------
|
19
|
-
np.ndarray
|
20
|
-
The inverted SSIM difference matrix, where higher values indicate greater dissimilarity between the two images.
|
21
|
-
"""
|
22
|
-
# Compute the structural similarity matrix (SSM) on the grayscale images
|
23
|
-
(score, diff) = ssm(ref_array, exp_array, full=True)
|
24
|
-
diff_inv = -diff
|
25
|
-
return diff_inv
|
26
|
-
|
27
|
-
def SP_BOS(ref_array : np.ndarray, exp_array : np.ndarray, binarization : str ="HPfilter", thresh : int = 128, freq : int = 500):
|
28
|
-
"""
|
29
|
-
Calculate the displacement map of stripe patterns in experimental images using the Background Oriented Schlieren (BOS) method.
|
30
|
-
|
31
|
-
This function computes the relative displacement between stripes in a reference and experimental image by compensating for background movement and noise. The displacement map is calculated by processing the images through several steps including image resizing, binarization, boundary detection, noise reduction, displacement calculation, and background compensation.
|
32
|
-
|
33
|
-
Parameters
|
34
|
-
----------
|
35
|
-
ref_array : np.ndarray
|
36
|
-
The reference grayscale image array. This image represents the original, undisturbed pattern.
|
37
|
-
|
38
|
-
exp_array : np.ndarray
|
39
|
-
The experimental grayscale image array. This image represents the pattern after deformation due to external factors.
|
40
|
-
|
41
|
-
binarization : str, optional, default="HPfilter"
|
42
|
-
The method used for binarization of the images. Options are:
|
43
|
-
- "thresh" : Use thresholding for binarization.
|
44
|
-
- "HPfilter" : Use high-pass filtering for binarization.
|
45
|
-
|
46
|
-
thresh : int, optional, default=128
|
47
|
-
The threshold value used for binarization when `binarization="thresh"`. Pixels with values above the threshold are set to 1, and those below are set to 0.
|
48
|
-
|
49
|
-
freq : int, optional, default=500
|
50
|
-
The frequency parameter used for high-pass filtering when `binarization="HPfilter"`.
|
51
|
-
|
52
|
-
Returns
|
53
|
-
-------
|
54
|
-
np.ndarray
|
55
|
-
A 2D array representing the displacement map of the stripe patterns, with background movement compensated. Each value represents the relative displacement between the reference and experimental images, with noise and background displacements removed.
|
56
|
-
|
57
|
-
Notes
|
58
|
-
-----
|
59
|
-
The method performs the following steps:
|
60
|
-
1. Vertically stretches both the reference and experimental images by a factor of 10.
|
61
|
-
2. Binarizes the images using either thresholding or high-pass filtering.
|
62
|
-
3. Identifies the upper and lower boundaries of the stripes and calculates their centers for both images.
|
63
|
-
4. Filters out noise by removing displacements larger than a certain threshold.
|
64
|
-
5. Computes the displacement between the stripe centers.
|
65
|
-
6. Compensates for background movement by normalizing the displacement map, subtracting the mean displacement over a specified region.
|
66
|
-
"""
|
67
|
-
|
68
|
-
im_ref=Image.fromarray(ref_array)
|
69
|
-
im_exp=Image.fromarray(exp_array)
|
70
|
-
|
71
|
-
#streach the image vertivally *10
|
72
|
-
im_ref=im_ref.resize((im_ref.size[0],im_ref.size[1]*10))
|
73
|
-
im_exp=im_exp.resize((im_exp.size[0],im_exp.size[1]*10))
|
74
|
-
|
75
|
-
ar_ref=np.array(im_ref)
|
76
|
-
ar_exp=np.array(im_exp)
|
77
|
-
|
78
|
-
if binarization =="thresh":
|
79
|
-
# Binarization
|
80
|
-
bin_ref = ib._biner_thresh(ar_ref, thresh)
|
81
|
-
bin_exp = ib._biner_thresh(ar_exp, thresh)
|
82
|
-
|
83
|
-
print("Binarization",bin_ref.shape,bin_exp.shape)
|
84
|
-
elif binarization =="HPfilter":
|
85
|
-
bin_ref=ib._biner_HP(ar_ref, freq)
|
86
|
-
bin_exp=ib._biner_HP(ar_exp, freq)
|
87
|
-
print("Binarization",bin_ref.shape,bin_exp.shape)
|
88
|
-
else:
|
89
|
-
raise ValueError("Binarization is thresh or HPfilter")
|
90
|
-
|
91
|
-
# Detect the coordinates of the color boundaries in the binarized reference image
|
92
|
-
ref_u, ref_d = ib._bin_indexer(bin_ref)
|
93
|
-
ref_u = np.nan_to_num(ref_u)
|
94
|
-
ref_d = np.nan_to_num(ref_d)
|
95
|
-
print("bin_indexer_ref",ref_u.shape,ref_d.shape)
|
96
|
-
# Detect the coordinates of the color boundaries in the binarized experimental image
|
97
|
-
# u represents the upper boundary of the white stripe, d represents the lower boundary
|
98
|
-
exp_u, exp_d = ib._bin_indexer(bin_exp)
|
99
|
-
exp_u = np.nan_to_num(exp_u)
|
100
|
-
exp_d = np.nan_to_num(exp_d)
|
101
|
-
print("bin_indexer_exp",exp_u.shape,exp_d.shape)
|
102
|
-
|
103
|
-
# Remove data with abnormally large displacements as noise
|
104
|
-
ref_u, exp_u = ib._noize_reducer_2(ref_u, exp_u, 10)
|
105
|
-
ref_d, exp_d = ib._noize_reducer_2(ref_d, exp_d, 10)
|
106
|
-
print("noize_reducer_2",exp_u.shape,exp_d.shape)
|
107
|
-
print("noize_reducer_2",ref_u.shape,ref_d.shape)
|
108
|
-
|
109
|
-
# Combine the upper and lower boundary data to calculate the center of the stripe
|
110
|
-
ref = ib._mixing(ref_u, ref_d)
|
111
|
-
exp = ib._mixing(exp_u, exp_d)
|
112
|
-
|
113
|
-
print("mixing",ref.shape,exp.shape)
|
114
|
-
|
115
|
-
# Calculate displacement (upward displacement is positive)
|
116
|
-
diff = -(exp - ref)
|
117
|
-
|
118
|
-
# Rearrange the displacement values into the correct positions and interpolate gaps
|
119
|
-
diff_comp = ib._complementer(ref, diff)
|
120
|
-
|
121
|
-
print("complementer",diff_comp.shape)
|
122
|
-
|
123
|
-
# Subtract the overall background movement by dividing by the mean displacement
|
124
|
-
diff_comp = diff_comp - np.nanmean(diff_comp[0:1000, 10:100])
|
125
|
-
|
126
|
-
return diff_comp
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|