pyvale 2025.4.1__py3-none-any.whl → 2025.5.2__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.
- pyvale/__init__.py +18 -3
- pyvale/analyticmeshgen.py +1 -0
- pyvale/analyticsimdatafactory.py +18 -13
- pyvale/analyticsimdatagenerator.py +105 -72
- pyvale/blendercalibrationdata.py +15 -0
- pyvale/blenderlightdata.py +26 -0
- pyvale/blendermaterialdata.py +15 -0
- pyvale/blenderrenderdata.py +30 -0
- pyvale/blenderscene.py +488 -0
- pyvale/blendertools.py +420 -0
- pyvale/camera.py +6 -5
- pyvale/cameradata.py +25 -7
- pyvale/cameradata2d.py +6 -4
- pyvale/camerastereo.py +217 -0
- pyvale/cameratools.py +206 -11
- pyvale/cython/rastercyth.py +6 -2
- pyvale/data/cal_target.tiff +0 -0
- pyvale/dataset.py +73 -14
- pyvale/errorcalculator.py +8 -10
- pyvale/errordriftcalc.py +10 -9
- pyvale/errorintegrator.py +19 -21
- pyvale/errorrand.py +33 -39
- pyvale/errorsyscalib.py +134 -0
- pyvale/errorsysdep.py +19 -22
- pyvale/errorsysfield.py +49 -41
- pyvale/errorsysindep.py +79 -175
- pyvale/examples/basics/ex1_1_basicscalars_therm2d.py +131 -0
- pyvale/examples/basics/ex1_2_sensormodel_therm2d.py +158 -0
- pyvale/examples/basics/ex1_3_customsens_therm3d.py +216 -0
- pyvale/examples/basics/ex1_4_basicerrors_therm3d.py +153 -0
- pyvale/examples/basics/ex1_5_fielderrs_therm3d.py +168 -0
- pyvale/examples/basics/ex1_6_caliberrs_therm2d.py +133 -0
- pyvale/examples/basics/ex1_7_spatavg_therm2d.py +123 -0
- pyvale/examples/basics/ex2_1_basicvectors_disp2d.py +112 -0
- pyvale/examples/basics/ex2_2_vectorsens_disp2d.py +111 -0
- pyvale/examples/basics/ex2_3_sensangle_disp2d.py +139 -0
- pyvale/examples/basics/ex2_4_chainfielderrs_disp2d.py +196 -0
- pyvale/examples/basics/ex2_5_vectorfields3d_disp3d.py +109 -0
- pyvale/examples/basics/ex3_1_basictensors_strain2d.py +114 -0
- pyvale/examples/basics/ex3_2_tensorsens2d_strain2d.py +111 -0
- pyvale/examples/basics/ex3_3_tensorsens3d_strain3d.py +182 -0
- pyvale/examples/basics/ex4_1_expsim2d_thermmech2d.py +171 -0
- pyvale/examples/basics/ex4_2_expsim3d_thermmech3d.py +252 -0
- pyvale/examples/{analyticdatagen → genanalyticdata}/ex1_1_scalarvisualisation.py +6 -9
- pyvale/examples/{analyticdatagen → genanalyticdata}/ex1_2_scalarcasebuild.py +8 -11
- pyvale/examples/{analyticdatagen → genanalyticdata}/ex2_1_analyticsensors.py +9 -12
- pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +8 -15
- pyvale/examples/renderblender/ex1_1_blenderscene.py +121 -0
- pyvale/examples/renderblender/ex1_2_blenderdeformed.py +119 -0
- pyvale/examples/renderblender/ex2_1_stereoscene.py +128 -0
- pyvale/examples/renderblender/ex2_2_stereodeformed.py +131 -0
- pyvale/examples/renderblender/ex3_1_blendercalibration.py +120 -0
- pyvale/examples/{rasterisation → renderrasterisation}/ex_rastenp.py +3 -2
- pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_oneframe.py +2 -2
- pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_static_cypara.py +3 -8
- pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_static_pypara.py +6 -7
- pyvale/examples/{ex1_4_thermal2d.py → visualisation/ex1_1_plot_traces.py} +32 -16
- pyvale/examples/{features/ex_animation_tools_3dmonoblock.py → visualisation/ex2_1_animate_sim.py} +37 -31
- pyvale/experimentsimulator.py +107 -30
- pyvale/field.py +2 -9
- pyvale/fieldconverter.py +98 -22
- pyvale/fieldsampler.py +2 -2
- pyvale/fieldscalar.py +10 -10
- pyvale/fieldtensor.py +15 -17
- pyvale/fieldtransform.py +7 -2
- pyvale/fieldvector.py +6 -7
- pyvale/generatorsrandom.py +25 -47
- pyvale/imagedef2d.py +6 -2
- pyvale/integratorfactory.py +2 -2
- pyvale/integratorquadrature.py +50 -24
- pyvale/integratorrectangle.py +85 -7
- pyvale/integratorspatial.py +4 -4
- pyvale/integratortype.py +3 -3
- pyvale/output.py +17 -0
- pyvale/pyvaleexceptions.py +11 -0
- pyvale/raster.py +6 -5
- pyvale/rastercy.py +6 -4
- pyvale/rasternp.py +6 -4
- pyvale/rendermesh.py +6 -2
- pyvale/sensorarray.py +2 -2
- pyvale/sensorarrayfactory.py +52 -65
- pyvale/sensorarraypoint.py +29 -30
- pyvale/sensordata.py +2 -2
- pyvale/sensordescriptor.py +138 -25
- pyvale/sensortools.py +3 -3
- pyvale/simtools.py +67 -0
- pyvale/visualexpplotter.py +99 -57
- pyvale/visualimagedef.py +11 -7
- pyvale/visualimages.py +6 -4
- pyvale/visualopts.py +372 -58
- pyvale/visualsimanimator.py +42 -13
- pyvale/visualsimsensors.py +318 -0
- pyvale/visualtools.py +69 -13
- pyvale/visualtraceplotter.py +52 -165
- {pyvale-2025.4.1.dist-info → pyvale-2025.5.2.dist-info}/METADATA +17 -14
- pyvale-2025.5.2.dist-info/RECORD +172 -0
- {pyvale-2025.4.1.dist-info → pyvale-2025.5.2.dist-info}/WHEEL +1 -1
- pyvale/examples/analyticdatagen/__init__.py +0 -5
- pyvale/examples/ex1_1_thermal2d.py +0 -86
- pyvale/examples/ex1_2_thermal2d.py +0 -108
- pyvale/examples/ex1_3_thermal2d.py +0 -110
- pyvale/examples/ex1_5_thermal2d.py +0 -102
- pyvale/examples/ex2_1_thermal3d .py +0 -84
- pyvale/examples/ex2_2_thermal3d.py +0 -51
- pyvale/examples/ex2_3_thermal3d.py +0 -106
- pyvale/examples/ex3_1_displacement2d.py +0 -44
- pyvale/examples/ex3_2_displacement2d.py +0 -76
- pyvale/examples/ex3_3_displacement2d.py +0 -101
- pyvale/examples/ex3_4_displacement2d.py +0 -102
- pyvale/examples/ex4_1_strain2d.py +0 -54
- pyvale/examples/ex4_2_strain2d.py +0 -76
- pyvale/examples/ex4_3_strain2d.py +0 -97
- pyvale/examples/ex5_1_multiphysics2d.py +0 -75
- pyvale/examples/ex6_1_multiphysics2d_expsim.py +0 -115
- pyvale/examples/ex6_2_multiphysics3d_expsim.py +0 -160
- pyvale/examples/features/__init__.py +0 -5
- pyvale/examples/features/ex_area_avg.py +0 -89
- pyvale/examples/features/ex_calibration_error.py +0 -108
- pyvale/examples/features/ex_chain_field_errs.py +0 -141
- pyvale/examples/features/ex_field_errs.py +0 -78
- pyvale/examples/features/ex_sensor_single_angle_batch.py +0 -110
- pyvale/optimcheckfuncs.py +0 -153
- pyvale/visualsimplotter.py +0 -182
- pyvale-2025.4.1.dist-info/RECORD +0 -163
- {pyvale-2025.4.1.dist-info → pyvale-2025.5.2.dist-info}/licenses/LICENSE +0 -0
- {pyvale-2025.4.1.dist-info → pyvale-2025.5.2.dist-info}/top_level.txt +0 -0
pyvale/camerastereo.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# pyvale: the python validation engine
|
|
3
|
+
# License: MIT
|
|
4
|
+
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
NOTE: This module is a feature under developement.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Self
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import numpy as np
|
|
14
|
+
import yaml
|
|
15
|
+
from scipy.spatial.transform import Rotation
|
|
16
|
+
from pyvale.cameradata import CameraData
|
|
17
|
+
from pyvale.pyvaleexceptions import BlenderError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CameraStereo:
|
|
21
|
+
__slots__ = ("cam_data_0","cam_data_1","stereo_dist","stereo_rotation")
|
|
22
|
+
|
|
23
|
+
def __init__(self, cam_data_0: CameraData, cam_data_1: CameraData) -> None:
|
|
24
|
+
self.cam_data_0 = cam_data_0
|
|
25
|
+
self.cam_data_1 = cam_data_1
|
|
26
|
+
|
|
27
|
+
cam0_rot_matrix = Rotation.as_matrix(self.cam_data_0.rot_world)
|
|
28
|
+
cam1_rot_matrix = Rotation.as_matrix(self.cam_data_1.rot_world)
|
|
29
|
+
(self.stereo_rotation, _) = Rotation.align_vectors(cam0_rot_matrix,
|
|
30
|
+
cam1_rot_matrix)
|
|
31
|
+
dist = self.cam_data_0.pos_world - self.cam_data_1.pos_world
|
|
32
|
+
dist_rot = self.cam_data_0.rot_world.apply(dist)
|
|
33
|
+
inverse = self.stereo_rotation.inv().as_quat()
|
|
34
|
+
inverse[3] *= -1
|
|
35
|
+
inverse = Rotation.from_quat(inverse)
|
|
36
|
+
self.stereo_dist = inverse.apply(dist_rot)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_calibration(cls,
|
|
40
|
+
calib_path: Path,
|
|
41
|
+
pos_world_0: np.ndarray,
|
|
42
|
+
rot_world_0: Rotation,
|
|
43
|
+
focal_length: float) -> Self:
|
|
44
|
+
"""A method to initialise the CameraStereo using a calibration file and
|
|
45
|
+
some additional parameters. This creates an instance of the CameraStereo
|
|
46
|
+
class from the calibration parameters.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
calib_path : Path
|
|
51
|
+
The path to the calibration file (in yaml format).
|
|
52
|
+
pos_world_0 : np.ndarray
|
|
53
|
+
The position of camera 0 in world coordinates.
|
|
54
|
+
rot_world_0 : Rotation
|
|
55
|
+
The rotation of camera 0 in world coordinates.
|
|
56
|
+
focal_length : float
|
|
57
|
+
The focal length of camera 0.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
Self
|
|
62
|
+
An instance of the CameraStereo class, given the specified parameters.
|
|
63
|
+
"""
|
|
64
|
+
calib_params = yaml.safe_load(calib_path.read_text())
|
|
65
|
+
pixels_num_cam0 = np.array([calib_params['Cam0_Cx [pixels]']*2,
|
|
66
|
+
calib_params['Cam0_Cy [pixels]']*2])
|
|
67
|
+
pixels_num_cam1 = np.array([calib_params['Cam1_Cx [pixels]']*2,
|
|
68
|
+
calib_params['Cam1_Cy [pixels]']*2])
|
|
69
|
+
pixels_size = focal_length / calib_params["Cam0_Fx [pixels]"]
|
|
70
|
+
stereo_rotation = Rotation.from_euler("xyz", ([calib_params['Theta [deg]'],
|
|
71
|
+
calib_params['Phi [deg]'],
|
|
72
|
+
calib_params['Psi [deg]']]), degrees=True)
|
|
73
|
+
stereo_dist = np.array([calib_params["Tx [mm]"],
|
|
74
|
+
calib_params["Ty [mm]"],
|
|
75
|
+
calib_params["Tz [mm]"]])
|
|
76
|
+
|
|
77
|
+
rot_world_1 = stereo_rotation * rot_world_0
|
|
78
|
+
|
|
79
|
+
inverse = stereo_rotation.inv().as_quat()
|
|
80
|
+
inverse[3] *= -1
|
|
81
|
+
inverse = Rotation.from_quat(inverse)
|
|
82
|
+
|
|
83
|
+
dist_rot = inverse.inv().apply(stereo_dist)
|
|
84
|
+
dist = rot_world_0.inv().apply(dist_rot)
|
|
85
|
+
pos_world_1 = pos_world_0 - dist
|
|
86
|
+
|
|
87
|
+
cam_data_0 = CameraData(pixels_num=pixels_num_cam0,
|
|
88
|
+
pixels_size=np.array([pixels_size, pixels_size]),
|
|
89
|
+
pos_world=pos_world_0,
|
|
90
|
+
rot_world=rot_world_0,
|
|
91
|
+
roi_cent_world=np.array([0, 0, 0]),
|
|
92
|
+
focal_length=focal_length)
|
|
93
|
+
cam_data_1 = CameraData(pixels_num=pixels_num_cam1,
|
|
94
|
+
pixels_size=np.array([pixels_size, pixels_size]),
|
|
95
|
+
pos_world=pos_world_1,
|
|
96
|
+
rot_world=rot_world_1,
|
|
97
|
+
roi_cent_world=np.array([0, 0, 0]),
|
|
98
|
+
focal_length=focal_length)
|
|
99
|
+
camera_stereo = cls(cam_data_0, cam_data_1)
|
|
100
|
+
|
|
101
|
+
return camera_stereo
|
|
102
|
+
|
|
103
|
+
def save_calibration(self, base_dir: Path) -> None:
|
|
104
|
+
"""A method to save a calibration file of the stereo system as a yaml.
|
|
105
|
+
This is so that the file can easily be read into python, but is also
|
|
106
|
+
user-readable.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
base_dir : Path
|
|
111
|
+
The base directory to which all files should be saved. The
|
|
112
|
+
calibration file will be saved in a sub-directory named "calibration"
|
|
113
|
+
within this directory.
|
|
114
|
+
|
|
115
|
+
Raises
|
|
116
|
+
------
|
|
117
|
+
BlenderError
|
|
118
|
+
"The specified save directory does not exist"
|
|
119
|
+
"""
|
|
120
|
+
stereo_rotation = self.stereo_rotation.as_euler("xyz", degrees=True)
|
|
121
|
+
calib_params = {
|
|
122
|
+
"Cam0_Fx [pixels]": float(self.cam_data_0.focal_length /
|
|
123
|
+
self.cam_data_0.pixels_size[0]),
|
|
124
|
+
"Cam0_Fy [pixels]": float(self.cam_data_0.focal_length /
|
|
125
|
+
self.cam_data_0.pixels_size[1]),
|
|
126
|
+
"Cam0_Fs [pixels]": 0,
|
|
127
|
+
"Cam0_Kappa 1": self.cam_data_0.k1,
|
|
128
|
+
"Cam0_Kappa 2": self.cam_data_0.k2,
|
|
129
|
+
"Cam0_Kappa 3": self.cam_data_0.k3,
|
|
130
|
+
"Cam0_P1": self.cam_data_0.p1,
|
|
131
|
+
"Cam0_P2": self.cam_data_0.p2,
|
|
132
|
+
"Cam0_Cx [pixels]": float(self.cam_data_0.c0),
|
|
133
|
+
"Cam0_Cy [pixels]": float(self.cam_data_0.c1),
|
|
134
|
+
"Cam1_Fx [pixels]": float(self.cam_data_1.focal_length /
|
|
135
|
+
self.cam_data_1.pixels_size[0]),
|
|
136
|
+
"Cam1_Fy [pixels]": float(self.cam_data_1.focal_length /
|
|
137
|
+
self.cam_data_1.pixels_size[1]),
|
|
138
|
+
"Cam1_Fs [pixels]": 0,
|
|
139
|
+
"Cam1_Kappa 1": self.cam_data_1.k1,
|
|
140
|
+
"Cam1_Kappa 2": self.cam_data_1.k2,
|
|
141
|
+
"Cam1_Kappa 3": self.cam_data_1.k3,
|
|
142
|
+
"Cam1_P1": self.cam_data_1.p1,
|
|
143
|
+
"Cam1_P2": self.cam_data_1.p2,
|
|
144
|
+
"Cam1_Cx [pixels]": float(self.cam_data_1.c0),
|
|
145
|
+
"Cam1_Cy [pixels]": float(self.cam_data_1.c1),
|
|
146
|
+
"Tx [mm]": float(self.stereo_dist[0]),
|
|
147
|
+
"Ty [mm]": float(self.stereo_dist[1]),
|
|
148
|
+
"Tz [mm]": float(self.stereo_dist[2]),
|
|
149
|
+
"Theta [deg]": float(stereo_rotation[0]),
|
|
150
|
+
"Phi [deg]": float(stereo_rotation[1]),
|
|
151
|
+
"Psi [deg]": float(stereo_rotation[2])
|
|
152
|
+
}
|
|
153
|
+
if not base_dir.is_dir():
|
|
154
|
+
raise BlenderError("The specified save directory does not exist")
|
|
155
|
+
|
|
156
|
+
save_dir = base_dir / "calibration"
|
|
157
|
+
if not save_dir.is_dir():
|
|
158
|
+
save_dir.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
|
|
160
|
+
filepath = str(save_dir / "calibration.yaml")
|
|
161
|
+
calib_file = open(filepath, "w")
|
|
162
|
+
yaml.safe_dump(calib_params, calib_file)
|
|
163
|
+
calib_file.close()
|
|
164
|
+
print("Calibration file saved to:", (save_dir / "calibration.yaml"))
|
|
165
|
+
|
|
166
|
+
def save_calibration_mid(self, base_dir: Path) -> None:
|
|
167
|
+
"""A method to save a calibration file of the stereo system in a MatchID
|
|
168
|
+
accepted format.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
base_dir : Path
|
|
173
|
+
The base directory to which all files should be saved. The
|
|
174
|
+
calibration file will be saved in a sub-directory named "calibration"
|
|
175
|
+
within this directory.
|
|
176
|
+
|
|
177
|
+
Raises
|
|
178
|
+
------
|
|
179
|
+
BlenderError
|
|
180
|
+
"The specified save directory does not exist"
|
|
181
|
+
"""
|
|
182
|
+
if not base_dir.is_dir():
|
|
183
|
+
raise BlenderError("The specified save directory does not exist")
|
|
184
|
+
|
|
185
|
+
save_dir = base_dir / "calibration"
|
|
186
|
+
if not save_dir.is_dir():
|
|
187
|
+
save_dir.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
|
|
189
|
+
filepath = str(save_dir / "calibration.caldat")
|
|
190
|
+
with open(filepath, "w") as file:
|
|
191
|
+
file.write(f'Cam0_Fx [pixels]; {self.cam_data_0.focal_length/ self.cam_data_0.pixels_size[0]}\n')
|
|
192
|
+
file.write(f'Cam0_Fy [pixels]; {self.cam_data_0.focal_length/ self.cam_data_0.pixels_size[1]}\n')
|
|
193
|
+
file.write("Cam0_Fs [pixels];0\n")
|
|
194
|
+
file.write(f'Cam0_Kappa 1;{self.cam_data_0.k1}\n')
|
|
195
|
+
file.write(f'Cam0_Kappa 2;{self.cam_data_0.k2}\n')
|
|
196
|
+
file.write(f'Cam0_Kappa 3;{self.cam_data_0.k3}\n')
|
|
197
|
+
file.write(f'Cam0_P1;{self.cam_data_0.p1}\n')
|
|
198
|
+
file.write(f'Cam0_P2;{self.cam_data_0.p2}\n')
|
|
199
|
+
file.write(f'Cam0_Cx [pixels];{self.cam_data_0.c0}\n')
|
|
200
|
+
file.write(f'Cam0_Cy [pixels];{self.cam_data_0.c1}\n')
|
|
201
|
+
file.write(f'Cam1_Fx [pixels]; {self.cam_data_1.focal_length/ self.cam_data_1.pixels_size[0]}\n')
|
|
202
|
+
file.write(f'Cam1_Fy [pixels]; {self.cam_data_1.focal_length/ self.cam_data_1.pixels_size[1]}\n')
|
|
203
|
+
file.write("Cam1_Fs [pixels];0\n")
|
|
204
|
+
file.write(f'Cam1_Kappa 1;{self.cam_data_1.k1}\n')
|
|
205
|
+
file.write(f'Cam1_Kappa 2;{self.cam_data_1.k2}\n')
|
|
206
|
+
file.write(f'Cam1_Kappa 3;{self.cam_data_1.k3}\n')
|
|
207
|
+
file.write(f'Cam1_P1;{self.cam_data_1.p1}\n')
|
|
208
|
+
file.write(f'Cam1_P2;{self.cam_data_1.p2}\n')
|
|
209
|
+
file.write(f'Cam1_Cx [pixels];{self.cam_data_1.c0}\n')
|
|
210
|
+
file.write(f'Cam1_Cy [pixels];{self.cam_data_1.c1}\n')
|
|
211
|
+
file.write(f"Tx [mm];{self.stereo_dist[0]}\n")
|
|
212
|
+
file.write(f"Ty [mm];{self.stereo_dist[1]}\n")
|
|
213
|
+
file.write(f"Tz [mm];{self.stereo_dist[2]}\n")
|
|
214
|
+
stereo_rotation = self.stereo_rotation.as_euler("xyz", degrees=True)
|
|
215
|
+
file.write(f"Theta [deg];{stereo_rotation[0]}\n")
|
|
216
|
+
file.write(f"Phi [deg];{stereo_rotation[1]}\n")
|
|
217
|
+
file.write(f"Psi [deg];{stereo_rotation[2]}")
|
pyvale/cameratools.py
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ==============================================================================
|
|
2
2
|
# pyvale: the python validation engine
|
|
3
3
|
# License: MIT
|
|
4
4
|
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
-
#
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
NOTE: This module is a feature under developement.
|
|
9
|
+
"""
|
|
6
10
|
|
|
7
11
|
import warnings
|
|
8
12
|
from pathlib import Path
|
|
9
13
|
import numpy as np
|
|
10
14
|
from scipy.signal import convolve2d
|
|
15
|
+
import copy
|
|
11
16
|
from scipy.spatial.transform import Rotation
|
|
12
17
|
import matplotlib.image as mplim
|
|
13
18
|
from PIL import Image
|
|
14
19
|
from pyvale.cameradata2d import CameraData2D
|
|
15
20
|
from pyvale.sensordata import SensorData
|
|
21
|
+
from pyvale.cameradata import CameraData
|
|
22
|
+
from pyvale.camerastereo import CameraStereo
|
|
16
23
|
|
|
17
|
-
# NOTE: This module is a feature under developement.
|
|
18
24
|
|
|
19
25
|
class CameraTools:
|
|
20
|
-
#-------------------------------------------------------------------------------
|
|
21
26
|
@staticmethod
|
|
22
27
|
def load_image(im_path: Path) -> np.ndarray:
|
|
23
28
|
|
|
@@ -55,7 +60,6 @@ class CameraTools:
|
|
|
55
60
|
|
|
56
61
|
return num_str
|
|
57
62
|
|
|
58
|
-
#-------------------------------------------------------------------------------
|
|
59
63
|
@staticmethod
|
|
60
64
|
def pixel_vec_px(pixels_count: np.ndarray) -> tuple[np.ndarray,np.ndarray]:
|
|
61
65
|
px_vec_x = np.arange(0,pixels_count[0],1)
|
|
@@ -71,7 +75,6 @@ class CameraTools:
|
|
|
71
75
|
(px_grid_x,px_grid_y) = CameraTools.pixel_grid_px(pixels_count)
|
|
72
76
|
return (px_grid_x.flatten(),px_grid_y.flatten())
|
|
73
77
|
|
|
74
|
-
#-------------------------------------------------------------------------------
|
|
75
78
|
@staticmethod
|
|
76
79
|
def subpixel_vec_px(pixels_count: np.ndarray,
|
|
77
80
|
subsample: int = 2) -> tuple[np.ndarray,np.ndarray]:
|
|
@@ -91,7 +94,6 @@ class CameraTools:
|
|
|
91
94
|
(px_grid_x,px_grid_y) = CameraTools.subpixel_grid_px(pixels_count,subsample)
|
|
92
95
|
return (px_grid_x.flatten(),px_grid_y.flatten())
|
|
93
96
|
|
|
94
|
-
#-------------------------------------------------------------------------------
|
|
95
97
|
@staticmethod
|
|
96
98
|
def pixel_vec_leng(field_of_view: np.ndarray,
|
|
97
99
|
leng_per_px: float) -> tuple[np.ndarray,np.ndarray]:
|
|
@@ -115,7 +117,7 @@ class CameraTools:
|
|
|
115
117
|
(px_grid_x,px_grid_y) = CameraTools.pixel_grid_leng(field_of_view,leng_per_px)
|
|
116
118
|
return (px_grid_x.flatten(),px_grid_y.flatten())
|
|
117
119
|
|
|
118
|
-
|
|
120
|
+
|
|
119
121
|
@staticmethod
|
|
120
122
|
def subpixel_vec_leng(field_of_view: np.ndarray,
|
|
121
123
|
leng_per_px: float,
|
|
@@ -148,7 +150,6 @@ class CameraTools:
|
|
|
148
150
|
subsample)
|
|
149
151
|
return (px_grid_x.flatten(),px_grid_y.flatten())
|
|
150
152
|
|
|
151
|
-
#-------------------------------------------------------------------------------
|
|
152
153
|
@staticmethod
|
|
153
154
|
def calc_resolution_from_sim_2d(pixels_count: np.ndarray,
|
|
154
155
|
coords: np.ndarray,
|
|
@@ -210,7 +211,6 @@ class CameraTools:
|
|
|
210
211
|
round(subsample/2)-1::subsample]
|
|
211
212
|
return avg_image
|
|
212
213
|
|
|
213
|
-
#---------------------------------------------------------------------------
|
|
214
214
|
@staticmethod
|
|
215
215
|
def build_sensor_data_from_camera_2d(cam_data: CameraData2D) -> SensorData:
|
|
216
216
|
pixels_vectorised = CameraTools.vectorise_pixel_grid_leng(cam_data.field_of_view,
|
|
@@ -324,4 +324,199 @@ class CameraTools:
|
|
|
324
324
|
return (roi_pos_world,cam_pos_world)
|
|
325
325
|
|
|
326
326
|
|
|
327
|
-
|
|
327
|
+
#---------------------------------------------------------------------------
|
|
328
|
+
# Blender camera tools
|
|
329
|
+
|
|
330
|
+
@staticmethod
|
|
331
|
+
def calculate_FOV(cam_data: CameraData) -> tuple[float, float]:
|
|
332
|
+
"""A method to calulate the camera's field of view in mm
|
|
333
|
+
|
|
334
|
+
Parameters
|
|
335
|
+
----------
|
|
336
|
+
cam_data : CameraData
|
|
337
|
+
A dataclass containing the camera parameters
|
|
338
|
+
|
|
339
|
+
Returns
|
|
340
|
+
-------
|
|
341
|
+
tuple[float, float]
|
|
342
|
+
A tuple containing the field of view in mm in both x and y directions
|
|
343
|
+
"""
|
|
344
|
+
FOV_x = (((cam_data.image_dist - cam_data.focal_length)
|
|
345
|
+
/ cam_data.focal_length) *
|
|
346
|
+
(cam_data.pixels_size) *
|
|
347
|
+
cam_data.pixels_num[0])[0]
|
|
348
|
+
FOV_y = (cam_data.pixels_num[1] / cam_data.pixels_num[0]) * FOV_x
|
|
349
|
+
FOV_mm = (FOV_x, FOV_y)
|
|
350
|
+
return FOV_mm
|
|
351
|
+
|
|
352
|
+
@staticmethod
|
|
353
|
+
def blender_FOV(cam_data: CameraData) -> tuple[float, float]:
|
|
354
|
+
"""A method to calculate the camera's field of view in mm using Blender's
|
|
355
|
+
method. This method differs due to one simplification.
|
|
356
|
+
|
|
357
|
+
Parameters
|
|
358
|
+
----------
|
|
359
|
+
cam_data : CameraData
|
|
360
|
+
A dataclass containing the camera parameters
|
|
361
|
+
|
|
362
|
+
Returns
|
|
363
|
+
-------
|
|
364
|
+
tuple[float, float]
|
|
365
|
+
A tuple containing the FOV in x and y directions
|
|
366
|
+
"""
|
|
367
|
+
FOV_x = (cam_data.pixels_num[0] * cam_data.pixels_size[0] * cam_data.image_dist) / cam_data.focal_length
|
|
368
|
+
FOV_y = (cam_data.pixels_num[1] / cam_data.pixels_num[0]) * FOV_x
|
|
369
|
+
FOV_blender = (FOV_x, FOV_y)
|
|
370
|
+
return FOV_blender
|
|
371
|
+
|
|
372
|
+
@staticmethod
|
|
373
|
+
def calculate_mm_px_resolution(cam_data: CameraData) -> float:
|
|
374
|
+
"""Function to calculate the mm/px resolution of a camera
|
|
375
|
+
|
|
376
|
+
Parameters
|
|
377
|
+
----------
|
|
378
|
+
cam_data : CameraData
|
|
379
|
+
A dataclass containing the camera parameters
|
|
380
|
+
|
|
381
|
+
Returns
|
|
382
|
+
-------
|
|
383
|
+
float
|
|
384
|
+
The mm/px resolution
|
|
385
|
+
"""
|
|
386
|
+
FOV_mm = CameraTools.blender_FOV(cam_data)
|
|
387
|
+
resolution = FOV_mm[0] / cam_data.pixels_num[0]
|
|
388
|
+
return resolution
|
|
389
|
+
|
|
390
|
+
@staticmethod
|
|
391
|
+
def focal_length_from_resolution(pixels_size: np.ndarray,
|
|
392
|
+
working_dist: float,
|
|
393
|
+
resolution: float) -> float:
|
|
394
|
+
"""A method to calculate the required focal length to achieve a certain
|
|
395
|
+
resolution. This is calculated given the pixel size and working distance.
|
|
396
|
+
This method can be used for a 2D setup or for camera 0 for a stereo setup.
|
|
397
|
+
|
|
398
|
+
Parameters
|
|
399
|
+
----------
|
|
400
|
+
pixels_size : np.ndarray
|
|
401
|
+
The camera pixel size in the x and y directions (in mm).
|
|
402
|
+
working_dist : float
|
|
403
|
+
The working distance of the camera to the sample.
|
|
404
|
+
resolution : float
|
|
405
|
+
The desired resolution in mm/px.
|
|
406
|
+
|
|
407
|
+
Returns
|
|
408
|
+
-------
|
|
409
|
+
float
|
|
410
|
+
The focal length required to obtain the desired image resolution.
|
|
411
|
+
"""
|
|
412
|
+
focal_length = working_dist / ((resolution / pixels_size[0]))
|
|
413
|
+
return focal_length
|
|
414
|
+
|
|
415
|
+
@staticmethod
|
|
416
|
+
def blender_camera_from_resolution(pixels_num: np.ndarray,
|
|
417
|
+
pixels_size: np.ndarray,
|
|
418
|
+
working_dist: float,
|
|
419
|
+
resolution: float) -> CameraData:
|
|
420
|
+
"""A convenience function to create a camera object in Blender from its pixels,
|
|
421
|
+
the pixel size, the working distance and desired resolution.
|
|
422
|
+
|
|
423
|
+
Parameters
|
|
424
|
+
----------
|
|
425
|
+
pixels_num : np.ndarray
|
|
426
|
+
The number of pixels in the camera, in the x and y directions.
|
|
427
|
+
pixels_size : np.ndarray
|
|
428
|
+
The camera pixels size in mm, in the x and y directions.
|
|
429
|
+
working_dist : float
|
|
430
|
+
The working distance of the camera.
|
|
431
|
+
resolution : float
|
|
432
|
+
The desired mm/px resolution
|
|
433
|
+
|
|
434
|
+
Returns
|
|
435
|
+
-------
|
|
436
|
+
CameraData
|
|
437
|
+
A dataclass containing the created camera's parameters.
|
|
438
|
+
"""
|
|
439
|
+
focal_length = CameraTools.focal_length_from_resolution(pixels_size, working_dist, resolution)
|
|
440
|
+
|
|
441
|
+
cam_data = CameraData(pixels_num=pixels_num,
|
|
442
|
+
pixels_size=pixels_size,
|
|
443
|
+
pos_world=(0, 0, working_dist),
|
|
444
|
+
rot_world=Rotation.from_euler("xyz", [0, 0, 0]),
|
|
445
|
+
roi_cent_world=(0, 0, 0),
|
|
446
|
+
focal_length=focal_length)
|
|
447
|
+
return cam_data
|
|
448
|
+
|
|
449
|
+
@staticmethod
|
|
450
|
+
def symmetric_stereo_cameras(cam_data_0: CameraData,
|
|
451
|
+
stereo_angle:float) -> CameraStereo:
|
|
452
|
+
"""A convenience function to set up a symmetric stereo camera system, given
|
|
453
|
+
an initial CameraData dataclass and a stereo angle. This assumes the basic
|
|
454
|
+
camera parameters are the same.
|
|
455
|
+
|
|
456
|
+
Parameters
|
|
457
|
+
----------
|
|
458
|
+
cam_data_0 : CameraData
|
|
459
|
+
A dataclass containing the camera parameters for a single camera, which
|
|
460
|
+
will be camera 0.
|
|
461
|
+
stereo_angle : float
|
|
462
|
+
The stereo angle between the two cameras.
|
|
463
|
+
|
|
464
|
+
Returns
|
|
465
|
+
-------
|
|
466
|
+
CameraStereo
|
|
467
|
+
An instance of the CameraStereo class. This class contains
|
|
468
|
+
information about each of the cameras, as well as the extrinsic
|
|
469
|
+
parameters between them.
|
|
470
|
+
"""
|
|
471
|
+
cam_data_1 = copy.deepcopy(cam_data_0)
|
|
472
|
+
base = 2 * cam_data_0.pos_world[2] * np.tan(np.radians(stereo_angle) / 2)
|
|
473
|
+
|
|
474
|
+
cam_data_0.pos_world[0] -= base / 2
|
|
475
|
+
cam_data_1.pos_world[0] += base / 2
|
|
476
|
+
|
|
477
|
+
cam_0_rot = (0, -np.radians(stereo_angle / 2), 0)
|
|
478
|
+
cam_0_rot = Rotation.from_euler("xyz", cam_0_rot, degrees=False)
|
|
479
|
+
cam_data_0.rot_world = cam_0_rot
|
|
480
|
+
|
|
481
|
+
cam_1_rot = (0, np.radians(stereo_angle / 2), 0)
|
|
482
|
+
cam_1_rot = Rotation.from_euler("xyz", cam_1_rot, degrees=False)
|
|
483
|
+
cam_data_1.rot_world = cam_1_rot
|
|
484
|
+
|
|
485
|
+
stereo_system = CameraStereo(cam_data_0, cam_data_1)
|
|
486
|
+
|
|
487
|
+
return stereo_system
|
|
488
|
+
|
|
489
|
+
@staticmethod
|
|
490
|
+
def faceon_stereo_cameras(cam_data_0: CameraData,
|
|
491
|
+
stereo_angle: float) -> CameraStereo:
|
|
492
|
+
# TODO: Correct docstring
|
|
493
|
+
"""A convenience function to set up a face-on stereo camera system, given
|
|
494
|
+
an initial CameraData dataclass and a stereo angle. This assumes the basic
|
|
495
|
+
camera parameters are the same.
|
|
496
|
+
|
|
497
|
+
Parameters
|
|
498
|
+
----------
|
|
499
|
+
cam_data_0 : CameraData
|
|
500
|
+
A dataclass containing the camera parameters for a single camera, which
|
|
501
|
+
will be camera 0.
|
|
502
|
+
stereo_angle : float
|
|
503
|
+
The stereo angle between the two cameras.
|
|
504
|
+
|
|
505
|
+
Returns
|
|
506
|
+
-------
|
|
507
|
+
CameraStereo
|
|
508
|
+
An instance of the CameraStereo class. This class contains
|
|
509
|
+
information about each of the cameras, as well as the extrinsic
|
|
510
|
+
parameters between them.
|
|
511
|
+
"""
|
|
512
|
+
cam_data_1 = copy.deepcopy(cam_data_0)
|
|
513
|
+
base = cam_data_0.pos_world[2] * np.tan(np.radians(stereo_angle))
|
|
514
|
+
cam_data_1.pos_world[0] += base
|
|
515
|
+
|
|
516
|
+
rotation_angle = (0, np.radians(stereo_angle), 0)
|
|
517
|
+
rotation_angle = Rotation.from_euler("xyz", rotation_angle, degrees=False)
|
|
518
|
+
cam_data_1.rot_world = rotation_angle
|
|
519
|
+
|
|
520
|
+
stereo_system = CameraStereo(cam_data_0, cam_data_1)
|
|
521
|
+
|
|
522
|
+
return stereo_system
|
pyvale/cython/rastercyth.py
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ==============================================================================
|
|
2
2
|
# pyvale: the python validation engine
|
|
3
3
|
# License: MIT
|
|
4
4
|
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
-
#
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
NOTE: this module is a feature under developement.
|
|
9
|
+
"""
|
|
6
10
|
|
|
7
11
|
import numpy as np
|
|
8
12
|
import cython
|
|
Binary file
|
pyvale/dataset.py
CHANGED
|
@@ -4,39 +4,63 @@
|
|
|
4
4
|
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
5
|
#===============================================================================
|
|
6
6
|
|
|
7
|
+
"""
|
|
8
|
+
Accesors for data that comes pre-packaged with pyvale for demonstrating its
|
|
9
|
+
functionality. This includes moose simulation outputs as exodus files, input
|
|
10
|
+
files for moose and gmsh for additional simulation cases, and images required
|
|
11
|
+
for testing the image deformation and digital image correlation modules.
|
|
12
|
+
"""
|
|
13
|
+
|
|
7
14
|
from enum import Enum
|
|
8
15
|
from pathlib import Path
|
|
9
16
|
from importlib.resources import files
|
|
10
17
|
|
|
11
18
|
|
|
19
|
+
SIM_CASE_COUNT = 26
|
|
20
|
+
"""Constant describing the number of simulation test case input files for moose
|
|
21
|
+
and gmsh that come packaged with pyvale.
|
|
22
|
+
"""
|
|
23
|
+
|
|
12
24
|
class EElemTest(Enum):
|
|
25
|
+
"""Enumeration used to specify different 3D element types for extracting
|
|
26
|
+
specific test simulation datasets.
|
|
27
|
+
"""
|
|
28
|
+
|
|
13
29
|
TET4 = "TET4"
|
|
30
|
+
"""Tetrahedral element, linear with 4 nodes.
|
|
31
|
+
"""
|
|
32
|
+
|
|
14
33
|
TET10 = "TET10"
|
|
34
|
+
"""Tetrahedral element, quadratic with 10 nodes.
|
|
35
|
+
"""
|
|
36
|
+
|
|
15
37
|
TET14 = "TET14"
|
|
38
|
+
"""Tetrahedral element, quadratic with 14 nodes.
|
|
39
|
+
"""
|
|
40
|
+
|
|
16
41
|
HEX8 = "HEX8"
|
|
42
|
+
"""Hexahedral element, linear with 8 nodes.
|
|
43
|
+
"""
|
|
44
|
+
|
|
17
45
|
HEX20 = "HEX20"
|
|
46
|
+
"""Hexahedral element, quadratic with 20 nodes.
|
|
47
|
+
"""
|
|
48
|
+
|
|
18
49
|
HEX27 = "HEX27"
|
|
50
|
+
"""Hexahedral element, quadratic with 27 nodes.
|
|
51
|
+
"""
|
|
19
52
|
|
|
20
53
|
def __str__(self):
|
|
21
54
|
return self.value
|
|
22
55
|
|
|
23
56
|
|
|
24
|
-
SIM_CASE_COUNT = 26
|
|
25
|
-
|
|
26
|
-
|
|
27
57
|
class DataSetError(Exception):
|
|
28
58
|
"""Custom error class for file io errors associated with retrieving datasets
|
|
29
59
|
and files packaged with pyvale.
|
|
30
60
|
"""
|
|
31
|
-
pass
|
|
32
61
|
|
|
33
62
|
|
|
34
63
|
class DataSet:
|
|
35
|
-
"""A static namespace class for handling datasets packaged with pyvale.
|
|
36
|
-
Contains a series of static methods returning a Path object to each data
|
|
37
|
-
file that is packaged with pyvale.
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
64
|
@staticmethod
|
|
41
65
|
def sim_case_input_file_path(case_num: int) -> Path:
|
|
42
66
|
"""Gets the path to MOOSE input file (*.i) for a particular simulation
|
|
@@ -62,7 +86,7 @@ class DataSet:
|
|
|
62
86
|
raise DataSetError("Simulation case number must be greater than 0")
|
|
63
87
|
elif case_num > SIM_CASE_COUNT:
|
|
64
88
|
raise DataSetError("Simulation case number must be less than " \
|
|
65
|
-
|
|
89
|
+
+ f"{SIM_CASE_COUNT}")
|
|
66
90
|
|
|
67
91
|
case_num_str = str(case_num).zfill(2)
|
|
68
92
|
case_file = f"case{case_num_str}.i"
|
|
@@ -97,7 +121,7 @@ class DataSet:
|
|
|
97
121
|
raise DataSetError("Simulation case number must be greater than 0")
|
|
98
122
|
elif case_num > SIM_CASE_COUNT:
|
|
99
123
|
raise DataSetError("Simulation case number must be less than " \
|
|
100
|
-
|
|
124
|
+
+ f"{SIM_CASE_COUNT}")
|
|
101
125
|
|
|
102
126
|
case_num_str = str(case_num).zfill(2)
|
|
103
127
|
case_file = f"case{case_num_str}.geo"
|
|
@@ -248,19 +272,54 @@ class DataSet:
|
|
|
248
272
|
|
|
249
273
|
@staticmethod
|
|
250
274
|
def render_mechanical_3d_path() -> Path:
|
|
251
|
-
"""
|
|
275
|
+
"""Path to a MOOSE simulation output in exodus format. This case is a
|
|
276
|
+
purely mechanical test case in 3D meant for testing image rendering
|
|
277
|
+
algorithms for digital image correlation simulation. The simulation
|
|
278
|
+
consists of a linear elastic thin plate with a hole loaded in tension.
|
|
279
|
+
The simulation uses linear tetrahedral elements for rendering tests.
|
|
252
280
|
|
|
253
281
|
Returns
|
|
254
282
|
-------
|
|
255
283
|
Path
|
|
256
|
-
|
|
284
|
+
Path to the exodus (*.e) output file for this simulation case.
|
|
257
285
|
"""
|
|
258
286
|
return Path(files("pyvale.data").joinpath("case26_out.e"))
|
|
259
287
|
|
|
260
288
|
@staticmethod
|
|
261
289
|
def render_simple_block_path() -> Path:
|
|
290
|
+
"""Path to a MOOSE simulation output in exodus format. This case is a
|
|
291
|
+
a simple rectangular block in 3D loaded in tension. It uses a minimum
|
|
292
|
+
number of elements and is intended purely for testing image rendering
|
|
293
|
+
algorithms. This simulation uses linear tetrahedral elements.
|
|
294
|
+
|
|
295
|
+
Returns
|
|
296
|
+
-------
|
|
297
|
+
Path
|
|
298
|
+
Path to the exodus (*.e) output file for this simulation case.
|
|
299
|
+
"""
|
|
262
300
|
return Path(files("pyvale.data").joinpath("case25_out.e"))
|
|
263
301
|
|
|
264
302
|
@staticmethod
|
|
265
303
|
def element_case_path(elem_type: EElemTest) -> Path:
|
|
266
|
-
|
|
304
|
+
"""Path to a MOOSE simulation output in exodus format. This case is a
|
|
305
|
+
10mm cube undergoing thermo-mechanical loading solved for the
|
|
306
|
+
temperature displacement and strain fields. This case is solved using a
|
|
307
|
+
variety of tetrahedral and hexahedral elements with linear or quadratic
|
|
308
|
+
shapes functions. These simulation cases are intended for testing
|
|
309
|
+
purposes and contain a minimal number of elements.
|
|
310
|
+
|
|
311
|
+
Parameters
|
|
312
|
+
----------
|
|
313
|
+
elem_type : EElemTest
|
|
314
|
+
Enumeration specifying the element type for this test case.
|
|
315
|
+
|
|
316
|
+
Returns
|
|
317
|
+
-------
|
|
318
|
+
Path
|
|
319
|
+
Path to the exodus (*.e) output file for this simulation case.
|
|
320
|
+
"""
|
|
321
|
+
return Path(files("pyvale.data")
|
|
322
|
+
.joinpath(f"case00_{elem_type.value}_out.e"))
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
|