pyvale 2025.4.0__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.

Potentially problematic release.


This version of pyvale might be problematic. Click here for more details.

Files changed (157) hide show
  1. pyvale/__init__.py +75 -0
  2. pyvale/core/__init__.py +7 -0
  3. pyvale/core/analyticmeshgen.py +59 -0
  4. pyvale/core/analyticsimdatafactory.py +63 -0
  5. pyvale/core/analyticsimdatagenerator.py +160 -0
  6. pyvale/core/camera.py +146 -0
  7. pyvale/core/cameradata.py +64 -0
  8. pyvale/core/cameradata2d.py +82 -0
  9. pyvale/core/cameratools.py +328 -0
  10. pyvale/core/cython/rastercyth.c +32267 -0
  11. pyvale/core/cython/rastercyth.py +636 -0
  12. pyvale/core/dataset.py +250 -0
  13. pyvale/core/errorcalculator.py +112 -0
  14. pyvale/core/errordriftcalc.py +146 -0
  15. pyvale/core/errorintegrator.py +339 -0
  16. pyvale/core/errorrand.py +614 -0
  17. pyvale/core/errorsysdep.py +331 -0
  18. pyvale/core/errorsysfield.py +407 -0
  19. pyvale/core/errorsysindep.py +905 -0
  20. pyvale/core/experimentsimulator.py +99 -0
  21. pyvale/core/field.py +136 -0
  22. pyvale/core/fieldconverter.py +154 -0
  23. pyvale/core/fieldsampler.py +112 -0
  24. pyvale/core/fieldscalar.py +167 -0
  25. pyvale/core/fieldtensor.py +221 -0
  26. pyvale/core/fieldtransform.py +384 -0
  27. pyvale/core/fieldvector.py +215 -0
  28. pyvale/core/generatorsrandom.py +528 -0
  29. pyvale/core/imagedef2d.py +566 -0
  30. pyvale/core/integratorfactory.py +241 -0
  31. pyvale/core/integratorquadrature.py +192 -0
  32. pyvale/core/integratorrectangle.py +88 -0
  33. pyvale/core/integratorspatial.py +90 -0
  34. pyvale/core/integratortype.py +44 -0
  35. pyvale/core/optimcheckfuncs.py +153 -0
  36. pyvale/core/raster.py +31 -0
  37. pyvale/core/rastercy.py +76 -0
  38. pyvale/core/rasternp.py +604 -0
  39. pyvale/core/rendermesh.py +156 -0
  40. pyvale/core/sensorarray.py +179 -0
  41. pyvale/core/sensorarrayfactory.py +210 -0
  42. pyvale/core/sensorarraypoint.py +280 -0
  43. pyvale/core/sensordata.py +72 -0
  44. pyvale/core/sensordescriptor.py +101 -0
  45. pyvale/core/sensortools.py +143 -0
  46. pyvale/core/visualexpplotter.py +151 -0
  47. pyvale/core/visualimagedef.py +71 -0
  48. pyvale/core/visualimages.py +75 -0
  49. pyvale/core/visualopts.py +180 -0
  50. pyvale/core/visualsimanimator.py +83 -0
  51. pyvale/core/visualsimplotter.py +182 -0
  52. pyvale/core/visualtools.py +81 -0
  53. pyvale/core/visualtraceplotter.py +256 -0
  54. pyvale/data/__init__.py +7 -0
  55. pyvale/data/case13_out.e +0 -0
  56. pyvale/data/case16_out.e +0 -0
  57. pyvale/data/case17_out.e +0 -0
  58. pyvale/data/case18_1_out.e +0 -0
  59. pyvale/data/case18_2_out.e +0 -0
  60. pyvale/data/case18_3_out.e +0 -0
  61. pyvale/data/case25_out.e +0 -0
  62. pyvale/data/case26_out.e +0 -0
  63. pyvale/data/optspeckle_2464x2056px_spec5px_8bit_gblur1px.tiff +0 -0
  64. pyvale/examples/__init__.py +7 -0
  65. pyvale/examples/analyticdatagen/__init__.py +7 -0
  66. pyvale/examples/analyticdatagen/ex1_1_scalarvisualisation.py +38 -0
  67. pyvale/examples/analyticdatagen/ex1_2_scalarcasebuild.py +46 -0
  68. pyvale/examples/analyticdatagen/ex2_1_analyticsensors.py +83 -0
  69. pyvale/examples/ex1_1_thermal2d.py +89 -0
  70. pyvale/examples/ex1_2_thermal2d.py +111 -0
  71. pyvale/examples/ex1_3_thermal2d.py +113 -0
  72. pyvale/examples/ex1_4_thermal2d.py +89 -0
  73. pyvale/examples/ex1_5_thermal2d.py +105 -0
  74. pyvale/examples/ex2_1_thermal3d .py +87 -0
  75. pyvale/examples/ex2_2_thermal3d.py +51 -0
  76. pyvale/examples/ex2_3_thermal3d.py +109 -0
  77. pyvale/examples/ex3_1_displacement2d.py +47 -0
  78. pyvale/examples/ex3_2_displacement2d.py +79 -0
  79. pyvale/examples/ex3_3_displacement2d.py +104 -0
  80. pyvale/examples/ex3_4_displacement2d.py +105 -0
  81. pyvale/examples/ex4_1_strain2d.py +57 -0
  82. pyvale/examples/ex4_2_strain2d.py +79 -0
  83. pyvale/examples/ex4_3_strain2d.py +100 -0
  84. pyvale/examples/ex5_1_multiphysics2d.py +78 -0
  85. pyvale/examples/ex6_1_multiphysics2d_expsim.py +118 -0
  86. pyvale/examples/ex6_2_multiphysics3d_expsim.py +158 -0
  87. pyvale/examples/features/__init__.py +7 -0
  88. pyvale/examples/features/ex_animation_tools_3dmonoblock.py +83 -0
  89. pyvale/examples/features/ex_area_avg.py +89 -0
  90. pyvale/examples/features/ex_calibration_error.py +108 -0
  91. pyvale/examples/features/ex_chain_field_errs.py +141 -0
  92. pyvale/examples/features/ex_field_errs.py +78 -0
  93. pyvale/examples/features/ex_sensor_single_angle_batch.py +110 -0
  94. pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +86 -0
  95. pyvale/examples/rasterisation/ex_rastenp.py +154 -0
  96. pyvale/examples/rasterisation/ex_rastercyth_oneframe.py +220 -0
  97. pyvale/examples/rasterisation/ex_rastercyth_static_cypara.py +194 -0
  98. pyvale/examples/rasterisation/ex_rastercyth_static_pypara.py +193 -0
  99. pyvale/simcases/case00_HEX20.i +242 -0
  100. pyvale/simcases/case00_HEX27.i +242 -0
  101. pyvale/simcases/case00_TET10.i +242 -0
  102. pyvale/simcases/case00_TET14.i +242 -0
  103. pyvale/simcases/case01.i +101 -0
  104. pyvale/simcases/case02.i +156 -0
  105. pyvale/simcases/case03.i +136 -0
  106. pyvale/simcases/case04.i +181 -0
  107. pyvale/simcases/case05.i +234 -0
  108. pyvale/simcases/case06.i +305 -0
  109. pyvale/simcases/case07.geo +135 -0
  110. pyvale/simcases/case07.i +87 -0
  111. pyvale/simcases/case08.geo +144 -0
  112. pyvale/simcases/case08.i +153 -0
  113. pyvale/simcases/case09.geo +204 -0
  114. pyvale/simcases/case09.i +87 -0
  115. pyvale/simcases/case10.geo +204 -0
  116. pyvale/simcases/case10.i +257 -0
  117. pyvale/simcases/case11.geo +337 -0
  118. pyvale/simcases/case11.i +147 -0
  119. pyvale/simcases/case12.geo +388 -0
  120. pyvale/simcases/case12.i +329 -0
  121. pyvale/simcases/case13.i +140 -0
  122. pyvale/simcases/case14.i +159 -0
  123. pyvale/simcases/case15.geo +337 -0
  124. pyvale/simcases/case15.i +150 -0
  125. pyvale/simcases/case16.geo +391 -0
  126. pyvale/simcases/case16.i +357 -0
  127. pyvale/simcases/case17.geo +135 -0
  128. pyvale/simcases/case17.i +144 -0
  129. pyvale/simcases/case18.i +254 -0
  130. pyvale/simcases/case18_1.i +254 -0
  131. pyvale/simcases/case18_2.i +254 -0
  132. pyvale/simcases/case18_3.i +254 -0
  133. pyvale/simcases/case19.geo +252 -0
  134. pyvale/simcases/case19.i +99 -0
  135. pyvale/simcases/case20.geo +252 -0
  136. pyvale/simcases/case20.i +250 -0
  137. pyvale/simcases/case21.geo +74 -0
  138. pyvale/simcases/case21.i +155 -0
  139. pyvale/simcases/case22.geo +82 -0
  140. pyvale/simcases/case22.i +140 -0
  141. pyvale/simcases/case23.geo +164 -0
  142. pyvale/simcases/case23.i +140 -0
  143. pyvale/simcases/case24.geo +79 -0
  144. pyvale/simcases/case24.i +123 -0
  145. pyvale/simcases/case25.geo +82 -0
  146. pyvale/simcases/case25.i +140 -0
  147. pyvale/simcases/case26.geo +166 -0
  148. pyvale/simcases/case26.i +140 -0
  149. pyvale/simcases/run_1case.py +61 -0
  150. pyvale/simcases/run_all_cases.py +69 -0
  151. pyvale/simcases/run_build_case.py +64 -0
  152. pyvale/simcases/run_example_cases.py +69 -0
  153. pyvale-2025.4.0.dist-info/METADATA +140 -0
  154. pyvale-2025.4.0.dist-info/RECORD +157 -0
  155. pyvale-2025.4.0.dist-info/WHEEL +5 -0
  156. pyvale-2025.4.0.dist-info/licenses/LICENSE +21 -0
  157. pyvale-2025.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,566 @@
1
+ """
2
+ ================================================================================
3
+ pyvale: the python validation engine
4
+ License: MIT
5
+ Copyright (C) 2025 The Computer Aided Validation Team
6
+ ================================================================================
7
+ """
8
+ import time
9
+ import warnings
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ import numpy as np
13
+ from scipy.interpolate import griddata
14
+ from scipy.interpolate import RectBivariateSpline
15
+ from scipy import ndimage
16
+
17
+ from pyvale.core.rasternp import edge_function, RasterNP
18
+ from pyvale.core.cameradata2d import CameraData2D
19
+ from pyvale.core.cameratools import CameraTools
20
+
21
+
22
+ @dataclass(slots=True)
23
+ class ImageDefOpts:
24
+ save_path: Path | None = None
25
+ save_tag: str = "defimage"
26
+
27
+ mask_input_image: bool = True
28
+
29
+ crop_on: bool = False
30
+ crop_px: np.ndarray | None = None # only used to crop input image if above is true
31
+
32
+ calc_res_from_fe: bool = False
33
+ calc_res_border_px: int = 5
34
+
35
+ add_static_ref: bool = False
36
+
37
+ fe_interp: str = "linear"
38
+ fe_rescale: bool = True
39
+ fe_extrap_outside_fov: bool = True # forces displacements outside the
40
+ #subsample: int = 2 # MOVED TO CAMERA DATA
41
+
42
+ image_def_order: int = 3
43
+ image_def_extrap: str = "nearest"
44
+ image_def_extval: float = 0.0 # only used if above is "constant"
45
+
46
+ def_complex_geom: bool = True
47
+
48
+ def __post_init__(self) -> None:
49
+ if self.save_path is None:
50
+ self.save_path = Path.cwd() / "deformed_images"
51
+
52
+
53
+ class ImageDef2D:
54
+
55
+ @staticmethod
56
+ def image_mask_from_sim(cam_data: CameraData2D,
57
+ image: np.ndarray,
58
+ coords: np.ndarray,
59
+ connectivity: np.ndarray
60
+ ) -> tuple[np.ndarray,np.ndarray]:
61
+
62
+ # Here to allow for addition
63
+ #subsample: int = cam_data.subsample
64
+ subsample: int = 1
65
+
66
+ coords_raster = coords - cam_data.roi_cent_world
67
+ if coords_raster.shape[1] >= 3:
68
+ coords_raster = coords_raster[:,:-1]
69
+
70
+ # Coords NDC: Convert to normalised device coords in the range [-1,1]
71
+ coords_raster[:,0] = 2*coords_raster[:,0] / cam_data.field_of_view[0]
72
+ coords_raster[:,1] = 2*coords_raster[:,1] / cam_data.field_of_view[1]
73
+
74
+ # Coords Raster: Covert to pixel (raster) coords
75
+ # Shape = ([X,Y,Z],num_nodes)
76
+ coords_raster[:,0] = (coords_raster[:,0] + 1)/2 * cam_data.pixels_count[0]
77
+ coords_raster[:,1] = (1-coords_raster[:,1])/2 * cam_data.pixels_count[1]
78
+
79
+ # shape=(num_elems,node_per_elem,coord[x,y])
80
+ elem_coords = np.ascontiguousarray(coords_raster[connectivity,:])
81
+
82
+ #shape=(num_elems,coord[x,y,z])
83
+ elem_coord_min = np.min(elem_coords,axis=1)
84
+ elem_coord_max = np.max(elem_coords,axis=1)
85
+
86
+ # Check that min/max nodes are within the 4 edges of the camera image
87
+ #shape=(4_edges_to_check,num_elems)
88
+ crop_mask = np.zeros([elem_coords.shape[0],4],dtype=np.int8)
89
+ crop_mask[elem_coord_min[:,0] <= (cam_data.pixels_count[0]-1), 0] = 1
90
+ crop_mask[elem_coord_min[:,1] <= (cam_data.pixels_count[1]-1), 1] = 1
91
+ crop_mask[elem_coord_max[:,0] >= 0, 2] = 1
92
+ crop_mask[elem_coord_max[:,1] >= 0, 3] = 1
93
+ crop_mask = np.sum(crop_mask,axis=1) == 4
94
+
95
+ # Mask the element coords
96
+ elem_coords = np.ascontiguousarray(elem_coords[crop_mask,:,:])
97
+
98
+ # Get only the elements that are within the FOV
99
+ # Mask the elem coords and the max and min elem coords for processing
100
+ elem_coord_min = elem_coord_min[crop_mask,:]
101
+ elem_coord_max = elem_coord_max[crop_mask,:]
102
+ num_elems_in_image = elem_coord_min.shape[0]
103
+
104
+ # Find the indices of the bounding box that each element lies within on
105
+ # the image, bounded by the upper and lower edges of the image
106
+ elem_bound_boxes_inds = np.zeros([num_elems_in_image,4],dtype=np.int32)
107
+ elem_bound_boxes_inds[:,0] = RasterNP.elem_bound_box_low(
108
+ elem_coord_min[:,0])
109
+ elem_bound_boxes_inds[:,1] = RasterNP.elem_bound_box_high(
110
+ elem_coord_max[:,0],
111
+ cam_data.pixels_count[0]-1)
112
+ elem_bound_boxes_inds[:,2] = RasterNP.elem_bound_box_low(
113
+ elem_coord_min[:,1])
114
+ elem_bound_boxes_inds[:,3] = RasterNP.elem_bound_box_high(
115
+ elem_coord_max[:,1],
116
+ cam_data.pixels_count[1]-1)
117
+
118
+ num_edges: int = 3
119
+ if elem_coords.shape[1] > 3:
120
+ num_edges = 4
121
+
122
+ mask_subpixel_buffer = np.full(subsample*cam_data.pixels_count,0.0).T
123
+ # Raster Loop
124
+ for ee in range(elem_coords.shape[0]):
125
+ # Create the subpixel coords inside the bounding box to test with the
126
+ # edge function. Use the pixel indices of the bounding box.
127
+ bound_subpx_x = np.arange(elem_bound_boxes_inds[ee,0],
128
+ elem_bound_boxes_inds[ee,1],
129
+ 1/subsample) + 1/(2*subsample)
130
+ bound_subpx_y = np.arange(elem_bound_boxes_inds[ee,2],
131
+ elem_bound_boxes_inds[ee,3],
132
+ 1/subsample) + 1/(2*subsample)
133
+ (bound_subpx_grid_x,bound_subpx_grid_y) = np.meshgrid(bound_subpx_x,
134
+ bound_subpx_y)
135
+ bound_coords_grid_shape = bound_subpx_grid_x.shape
136
+ # shape=(coord[x,y],num_subpx_in_box)
137
+ bound_subpx_coords_flat = np.vstack((bound_subpx_grid_x.flatten(),
138
+ bound_subpx_grid_y.flatten()))
139
+
140
+ # Create the subpixel indices for buffer slicing later
141
+ subpx_inds_x = np.arange(subsample*elem_bound_boxes_inds[ee,0],
142
+ subsample*elem_bound_boxes_inds[ee,1])
143
+ subpx_inds_y = np.arange(subsample*elem_bound_boxes_inds[ee,2],
144
+ subsample*elem_bound_boxes_inds[ee,3])
145
+ (subpx_inds_grid_x,subpx_inds_grid_y) = np.meshgrid(subpx_inds_x,
146
+ subpx_inds_y)
147
+
148
+ edge = np.zeros((num_edges,bound_subpx_coords_flat.shape[1]),dtype=np.float64)
149
+
150
+ if num_edges == 4:
151
+ edge[0,:] = edge_function(elem_coords[ee,1,:],
152
+ elem_coords[ee,2,:],
153
+ bound_subpx_coords_flat)
154
+ edge[1,:] = edge_function(elem_coords[ee,2,:],
155
+ elem_coords[ee,3,:],
156
+ bound_subpx_coords_flat)
157
+ edge[2,:] = edge_function(elem_coords[ee,3,:],
158
+ elem_coords[ee,0,:],
159
+ bound_subpx_coords_flat)
160
+ edge[3,:] = edge_function(elem_coords[ee,0,:],
161
+ elem_coords[ee,1,:],
162
+ bound_subpx_coords_flat)
163
+ else:
164
+ edge[0,:] = edge_function(elem_coords[ee,1,:],
165
+ elem_coords[ee,2,:],
166
+ bound_subpx_coords_flat)
167
+ edge[1,:] = edge_function(elem_coords[ee,2,:],
168
+ elem_coords[ee,0,:],
169
+ bound_subpx_coords_flat)
170
+ edge[2,:] = edge_function(elem_coords[ee,0,:],
171
+ elem_coords[ee,1,:],
172
+ bound_subpx_coords_flat)
173
+
174
+
175
+ # Now we check where the edge function is above zero for all edges
176
+ edge_check = np.zeros_like(edge,dtype=np.int8)
177
+ edge_check[edge >= 0.0] = 1
178
+ edge_check = np.sum(edge_check, axis=0)
179
+ # Create a mask with the check, TODO check the 3 here for non triangles
180
+ edge_mask_flat = edge_check == num_edges
181
+ edge_mask_grid = np.reshape(edge_mask_flat,bound_coords_grid_shape)
182
+
183
+ subpx_inds_grid_x = subpx_inds_grid_x[edge_mask_grid]
184
+ subpx_inds_grid_y = subpx_inds_grid_y[edge_mask_grid]
185
+ mask_subpixel_buffer[subpx_inds_grid_y,subpx_inds_grid_x] += 1.0
186
+
187
+ mask_subpixel_buffer[mask_subpixel_buffer>1.0] = 1.0
188
+
189
+ mask_buffer = CameraTools.average_subpixel_image(mask_subpixel_buffer,
190
+ subsample)
191
+ image[mask_buffer<1.0] = cam_data.background
192
+ return (image,mask_subpixel_buffer)
193
+
194
+
195
+ @staticmethod
196
+ def upsample_image(cam_data: CameraData2D,
197
+ input_im: np.ndarray):
198
+ # Get grid of pixel centroid locations
199
+ (px_vec_xm,px_vec_ym) = CameraTools.pixel_vec_leng(cam_data.field_of_view,
200
+ cam_data.leng_per_px)
201
+
202
+ # Get grid of sub-pixel centroid locations
203
+ (subpx_vec_xm,subpx_vec_ym) = CameraTools.subpixel_vec_leng(
204
+ cam_data.field_of_view,
205
+ cam_data.leng_per_px,
206
+ cam_data.subsample)
207
+
208
+ # NOTE: See Scipy transition from interp2d docs here:
209
+ # https://scipy.github.io/devdocs/tutorial/interpolate/interp_transition_guide.html
210
+ spline_interp = RectBivariateSpline(px_vec_xm,
211
+ px_vec_ym,
212
+ input_im.T)
213
+ upsampled_image_interp = lambda x_new, y_new: spline_interp(x_new, y_new).T
214
+
215
+ # This function will flip the image regardless of the y vector input so flip it
216
+ # back to FE coords
217
+ upsampled_image = upsampled_image_interp(subpx_vec_xm,subpx_vec_ym)
218
+
219
+ return upsampled_image
220
+
221
+
222
+ @staticmethod
223
+ def preprocess(cam_data: CameraData2D,
224
+ image_input: np.ndarray,
225
+ coords: np.ndarray,
226
+ connectivity: np.ndarray,
227
+ disp_x: np.ndarray,
228
+ disp_y: np.ndarray,
229
+ id_opts: ImageDefOpts,
230
+ print_on: bool = False
231
+ ) -> tuple[np.ndarray | None,
232
+ np.ndarray | None,
233
+ np.ndarray | None,
234
+ np.ndarray | None,
235
+ np.ndarray | None]:
236
+
237
+ if print_on:
238
+ print("\n"+"="*80)
239
+ print("IMAGE DEF PRE-PROCESSING\n")
240
+
241
+ if not id_opts.save_path.is_dir():
242
+ id_opts.save_path.mkdir()
243
+
244
+ # Make displacements a 2D column vector, allows addition of static frame
245
+ if disp_x.ndim == 1:
246
+ disp_x = np.atleast_2d(disp_x).T
247
+ if disp_y.ndim == 1:
248
+ disp_y = np.atleast_2d(disp_y).T
249
+
250
+ if id_opts.add_static_ref:
251
+ num_nodes = coords.shape[0] # type: ignore
252
+ disp_x = np.hstack((np.zeros((num_nodes,1)),disp_x))
253
+ disp_y = np.hstack((np.zeros((num_nodes,1)),disp_y))
254
+
255
+ image_input = CameraTools.crop_image_rectangle(image_input,
256
+ cam_data.pixels_count)
257
+
258
+ if id_opts.mask_input_image or id_opts.def_complex_geom:
259
+ if print_on:
260
+ print('Image masking or complex geometry on, getting image mask.')
261
+ tic = time.perf_counter()
262
+
263
+ (image_input,
264
+ image_mask) = ImageDef2D.image_mask_from_sim(cam_data,
265
+ image_input,
266
+ coords,
267
+ connectivity)
268
+
269
+
270
+ if print_on:
271
+ toc = time.perf_counter()
272
+ print(f'Calculating image mask took {toc-tic:.4f} seconds')
273
+ else:
274
+ image_mask = None
275
+
276
+
277
+ # Image upsampling
278
+ if print_on:
279
+ print('\n'+'-'*80)
280
+ print('GENERATE UPSAMPLED IMAGE\n')
281
+ print(f'Upsampling input image with a {cam_data.subsample}x{cam_data.subsample} subpixel')
282
+ tic = time.perf_counter()
283
+
284
+ upsampled_image = ImageDef2D.upsample_image(cam_data,image_input)
285
+
286
+ if print_on:
287
+ toc = time.perf_counter()
288
+ print(f'Upsampling image withtook {toc-tic:.4f} seconds')
289
+
290
+ return (upsampled_image,image_mask,image_input,disp_x,disp_y)
291
+
292
+ @staticmethod
293
+ def deform_one_image(upsampled_image: np.ndarray,
294
+ cam_data: CameraData2D,
295
+ id_opts: ImageDefOpts,
296
+ coords: np.ndarray,
297
+ disp: np.ndarray,
298
+ image_mask: np.ndarray | None = None,
299
+ print_on: bool = True
300
+ ) -> tuple[np.ndarray,
301
+ np.ndarray,
302
+ np.ndarray,
303
+ np.ndarray,
304
+ np.ndarray | None]:
305
+
306
+ if image_mask is not None:
307
+ if (image_mask.shape[0] != cam_data.pixels_count[1]) or (image_mask.shape[1] != cam_data.pixels_count[0]):
308
+ if image_mask.size == 0:
309
+ warnings.warn('Image mask not specified, using default mask of whole image.')
310
+ else:
311
+ warnings.warn('Image mask size does not match camera, using default mask of whole image.')
312
+ image_mask = np.ones([cam_data.pixels_count[1],cam_data.pixels_count[0]])
313
+
314
+ (px_grid_xm,
315
+ px_grid_ym) = CameraTools.pixel_grid_leng(cam_data.field_of_view,
316
+ cam_data.leng_per_px)
317
+
318
+ (subpx_grid_xm,
319
+ subpx_grid_ym) = CameraTools.subpixel_grid_leng(cam_data.field_of_view,
320
+ cam_data.leng_per_px,
321
+ cam_data.subsample)
322
+
323
+ #--------------------------------------------------------------------------
324
+ # Interpolate FE displacements onto the sub-pixel grid
325
+ if print_on:
326
+ print('Interpolating displacement onto sub-pixel grid.')
327
+ tic = time.perf_counter()
328
+
329
+ (subpx_disp_x,subpx_disp_y) = _interp_sim_disp_to_subpx_grid(
330
+ coords,
331
+ disp,
332
+ cam_data,
333
+ id_opts,
334
+ subpx_grid_xm,
335
+ subpx_grid_ym)
336
+
337
+ if print_on:
338
+ toc = time.perf_counter()
339
+ print('Interpolating displacement with NaN extrap took {:.4f} seconds'.format(toc-tic))
340
+
341
+ #--------------------------------------------------------------------------
342
+ # Interpolate sub-pixel gray levels with ndimage toolbox
343
+ if print_on:
344
+ print('Deforming sub-pixel image.')
345
+ tic = time.perf_counter()
346
+
347
+ def_image_subpx = _interp_subpx_image(upsampled_image,
348
+ subpx_grid_xm-subpx_disp_x,
349
+ subpx_grid_ym-subpx_disp_y,
350
+ cam_data,
351
+ id_opts)
352
+
353
+ if print_on:
354
+ toc = time.perf_counter()
355
+ print('Deforming sub-pixel image with ndimage took {:.4f} seconds'.format(toc-tic))
356
+
357
+ #--------------------------------------------------------------------------
358
+ # Average subpixel image
359
+ if print_on:
360
+ tic = time.perf_counter()
361
+
362
+ def_image = CameraTools.average_subpixel_image(def_image_subpx,
363
+ cam_data.subsample)
364
+
365
+ if print_on:
366
+ toc = time.perf_counter()
367
+ print('Averaging sub-pixel imagetook {:.4f} seconds'.format(toc-tic))
368
+
369
+ #--------------------------------------------------------------------------
370
+ # DEFORMING IMAGE MASK
371
+ if id_opts.def_complex_geom:
372
+ if print_on:
373
+ print('Deforming image mask.')
374
+ tic = time.perf_counter()
375
+
376
+ (def_image,def_mask) = _deform_image_mask(def_image,
377
+ image_mask,
378
+ px_grid_xm,
379
+ px_grid_ym,
380
+ subpx_disp_x,
381
+ subpx_disp_y,
382
+ cam_data)
383
+
384
+ if print_on:
385
+ toc = time.perf_counter()
386
+ print('Deforming image mask with ndimage took {:.4f} seconds'.format(toc-tic))
387
+
388
+ else:
389
+ def_mask = None
390
+
391
+ # Need to flip the image as all processing above is done with y axis down
392
+ # from the top left hand corner
393
+ def_image = def_image[::-1,:]
394
+
395
+ return (def_image,def_image_subpx,subpx_disp_x,subpx_disp_y,def_mask)
396
+
397
+ @staticmethod
398
+ def deform_images_to_disk(cam_data: CameraData2D,
399
+ upsampled_image: np.ndarray,
400
+ coords: np.ndarray,
401
+ connectivity: np.ndarray,
402
+ disp_x: np.ndarray,
403
+ disp_y: np.ndarray,
404
+ image_mask: np.ndarray | None,
405
+ id_opts: ImageDefOpts,
406
+ print_on: bool = False) -> None:
407
+
408
+ #---------------------------------------------------------------------------
409
+ # Image Deformation Loop
410
+ if print_on:
411
+ print('\n'+'='*80)
412
+ print('DEFORMING IMAGES')
413
+
414
+ num_frames = disp_x.shape[1]
415
+ ticl = time.perf_counter()
416
+
417
+ for ff in range(num_frames):
418
+ if print_on:
419
+ ticf = time.perf_counter()
420
+ print(f'\nDEFORMING FRAME: {ff}')
421
+
422
+ disp = np.array((disp_x[:,ff],disp_y[:,ff])).T
423
+ (def_image,
424
+ _,
425
+ _,
426
+ _,
427
+ _) = ImageDef2D.deform_one_image(upsampled_image,
428
+ cam_data,
429
+ id_opts,
430
+ coords,
431
+ disp,
432
+ image_mask,
433
+ print_on=print_on)
434
+
435
+ save_file = id_opts.save_path / str(f'{id_opts.save_tag}_'+
436
+ f'{CameraTools.image_num_str(im_num=ff,width=4)}'+
437
+ '.tiff')
438
+ CameraTools.save_image(save_file,def_image,cam_data.bits)
439
+
440
+ if print_on:
441
+ tocf = time.perf_counter()
442
+ print(f'DEFORMING FRAME: {ff} took {tocf-ticf:.4f} seconds')
443
+
444
+ if print_on:
445
+ tocl = time.perf_counter()
446
+ print('\n'+'-'*50)
447
+ print(f'Deforming all images took {tocl-ticl:.4f} seconds')
448
+ print('-'*50)
449
+
450
+ print('\n'+'='*80)
451
+ print('COMPLETE\n')
452
+
453
+
454
+
455
+ def _interp_sim_disp_to_subpx_grid(coords: np.ndarray,
456
+ disp: np.ndarray,
457
+ cam_data: CameraData2D,
458
+ id_opts: ImageDefOpts,
459
+ subpx_grid_xm: np.ndarray,
460
+ subpx_grid_ym: np.ndarray
461
+ ) -> tuple[np.ndarray,np.ndarray]:
462
+
463
+ # Interpolate displacements onto sub-pixel locations - nan extrapolation
464
+ subpx_disp_x = griddata((coords[:,0] + disp[:,0] + cam_data.world_to_cam[0],
465
+ coords[:,1] + disp[:,1] + cam_data.world_to_cam[1]),
466
+ disp[:,0],
467
+ (subpx_grid_xm,subpx_grid_ym),
468
+ method=id_opts.fe_interp,
469
+ fill_value=np.nan,
470
+ rescale=id_opts.fe_rescale)
471
+
472
+ subpx_disp_y = griddata((coords[:,0] + disp[:,0] + cam_data.world_to_cam[0],
473
+ coords[:,1] + disp[:,1] + cam_data.world_to_cam[1]),
474
+ disp[:,1],
475
+ (subpx_grid_xm,subpx_grid_ym),
476
+ method=id_opts.fe_interp,
477
+ fill_value=np.nan,
478
+ rescale=id_opts.fe_rescale)
479
+
480
+ # Ndimage interp can't handle nans so force everything outside the specimen
481
+ # to extrapolate outside the FOV - then use ndimage opts to control
482
+ if id_opts.fe_extrap_outside_fov:
483
+ subpx_disp_ext_vals = 2*cam_data.field_of_view
484
+ else:
485
+ subpx_disp_ext_vals = (0.0,0.0)
486
+
487
+ # Set the nans to the extrapoltion value
488
+ subpx_disp_x[np.isnan(subpx_disp_x)] = subpx_disp_ext_vals[0]
489
+ subpx_disp_y[np.isnan(subpx_disp_y)] = subpx_disp_ext_vals[1]
490
+
491
+ return (subpx_disp_x,subpx_disp_y)
492
+
493
+
494
+ def _interp_subpx_image(upsampled_image: np.ndarray,
495
+ def_subpx_x: np.ndarray,
496
+ def_subpx_y: np.ndarray,
497
+ cam_data: CameraData2D,
498
+ id_opts: ImageDefOpts,
499
+ ) -> np.ndarray:
500
+
501
+ # Flip needed to be consistent with pixel coords of ndimage
502
+ def_subpx_x = def_subpx_x[::-1,:]
503
+ def_subpx_y = def_subpx_y[::-1,:]
504
+
505
+ # NDIMAGE: IMAGE DEF
506
+ # NOTE: need to shift to pixel centroid co-ords from nodal so -0.5 makes the
507
+ # top left 0,0 in pixel co-ords
508
+ def_subpx_x_in_px = def_subpx_x*(cam_data.subsample/cam_data.leng_per_px)-0.5
509
+ def_subpx_y_in_px = def_subpx_y*(cam_data.subsample/cam_data.leng_per_px)-0.5
510
+ # NOTE: prefilter needs to be on to match griddata and interp2D!
511
+ # with prefilter on this exactly matches I2D but 10x faster!
512
+ def_image_subpx = ndimage.map_coordinates(upsampled_image,
513
+ [[def_subpx_y_in_px],
514
+ [def_subpx_x_in_px]],
515
+ prefilter=True,
516
+ order= id_opts.image_def_order,
517
+ mode= id_opts.image_def_extrap,
518
+ cval= id_opts.image_def_extval)
519
+
520
+ def_image_subpx = def_image_subpx[0,:,:].squeeze()
521
+
522
+ return def_image_subpx
523
+
524
+
525
+ def _deform_image_mask(def_image: np.ndarray,
526
+ image_mask: np.ndarray,
527
+ px_grid_xm: np.ndarray,
528
+ px_grid_ym: np.ndarray,
529
+ subpx_disp_x: np.ndarray,
530
+ subpx_disp_y: np.ndarray,
531
+ cam_data: CameraData2D,
532
+ ) -> tuple[np.ndarray,np.ndarray]:
533
+
534
+ # This is slow - might be quicker to just deform an upsampled mask
535
+ px_disp_x = CameraTools.average_subpixel_image(subpx_disp_x,
536
+ cam_data.subsample)
537
+ px_disp_y = CameraTools.average_subpixel_image(subpx_disp_y,
538
+ cam_data.subsample)
539
+
540
+ def_px_x = px_grid_xm-px_disp_x
541
+ def_px_y = px_grid_ym-px_disp_y
542
+ # Flip needed to be consistent with pixel coords of ndimage
543
+ def_px_x = def_px_x[::-1,:]
544
+ def_px_y = def_px_y[::-1,:]
545
+
546
+ # NDIMAGE: DEFORM IMAGE MASK
547
+ # NOTE: need to shift to pixel centroid co-ords from nodal so -0.5 makes the
548
+ # top left 0,0 in pixel co-ords
549
+ def_px_x_in_px = def_px_x*(1/cam_data.leng_per_px)-0.5
550
+ def_px_y_in_px = def_px_y*(1/cam_data.leng_per_px)-0.5
551
+ # NOTE: prefilter needs to be on to match griddata and interp2D!
552
+ # with prefilter on this exactly matches I2D but 10x faster!
553
+ def_mask = ndimage.map_coordinates(image_mask,
554
+ [[def_px_y_in_px],
555
+ [def_px_x_in_px]],
556
+ prefilter=True,
557
+ order=2,
558
+ mode='constant',
559
+ cval=0)
560
+
561
+ def_mask = def_mask[0,:,:].squeeze()
562
+ # Use the deformed image mask to mask the deformed image
563
+ # Mask is 0-1 with 1 being definitely inside the sample 0 outside
564
+ def_image[def_mask<0.51] = cam_data.background # type: ignore
565
+
566
+ return (def_image,image_mask)