pyvale 2025.4.0__py3-none-any.whl → 2025.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyvale might be problematic. Click here for more details.
- pyvale/__init__.py +78 -64
- pyvale/analyticmeshgen.py +102 -0
- pyvale/{core/analyticsimdatafactory.py → analyticsimdatafactory.py} +44 -16
- pyvale/analyticsimdatagenerator.py +323 -0
- 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/{core/camera.py → camera.py} +15 -15
- pyvale/{core/cameradata.py → cameradata.py} +27 -22
- pyvale/{core/cameradata2d.py → cameradata2d.py} +8 -6
- pyvale/camerastereo.py +217 -0
- pyvale/{core/cameratools.py → cameratools.py} +220 -26
- pyvale/{core/cython → cython}/rastercyth.py +11 -7
- pyvale/data/__init__.py +5 -7
- pyvale/data/cal_target.tiff +0 -0
- pyvale/data/case00_HEX20_out.e +0 -0
- pyvale/data/case00_HEX27_out.e +0 -0
- pyvale/data/case00_HEX8_out.e +0 -0
- pyvale/data/case00_TET10_out.e +0 -0
- pyvale/data/case00_TET14_out.e +0 -0
- pyvale/data/case00_TET4_out.e +0 -0
- pyvale/{core/dataset.py → dataset.py} +91 -16
- pyvale/{core/errorcalculator.py → errorcalculator.py} +13 -16
- pyvale/{core/errordriftcalc.py → errordriftcalc.py} +14 -14
- pyvale/{core/errorintegrator.py → errorintegrator.py} +25 -28
- pyvale/{core/errorrand.py → errorrand.py} +39 -46
- pyvale/errorsyscalib.py +134 -0
- pyvale/{core/errorsysdep.py → errorsysdep.py} +25 -29
- pyvale/{core/errorsysfield.py → errorsysfield.py} +59 -52
- pyvale/{core/errorsysindep.py → errorsysindep.py} +85 -182
- pyvale/examples/__init__.py +5 -7
- 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 +6 -7
- pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_oneframe.py +5 -7
- pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_static_cypara.py +6 -13
- pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_static_pypara.py +9 -12
- pyvale/examples/{ex1_4_thermal2d.py → visualisation/ex1_1_plot_traces.py} +33 -20
- pyvale/examples/{features/ex_animation_tools_3dmonoblock.py → visualisation/ex2_1_animate_sim.py} +37 -31
- pyvale/experimentsimulator.py +175 -0
- pyvale/{core/field.py → field.py} +6 -14
- pyvale/fieldconverter.py +351 -0
- pyvale/{core/fieldsampler.py → fieldsampler.py} +9 -10
- pyvale/{core/fieldscalar.py → fieldscalar.py} +17 -18
- pyvale/{core/fieldtensor.py → fieldtensor.py} +23 -26
- pyvale/{core/fieldtransform.py → fieldtransform.py} +9 -5
- pyvale/{core/fieldvector.py → fieldvector.py} +14 -16
- pyvale/{core/generatorsrandom.py → generatorsrandom.py} +29 -52
- pyvale/{core/imagedef2d.py → imagedef2d.py} +11 -8
- pyvale/{core/integratorfactory.py → integratorfactory.py} +12 -13
- pyvale/{core/integratorquadrature.py → integratorquadrature.py} +57 -32
- pyvale/integratorrectangle.py +165 -0
- pyvale/{core/integratorspatial.py → integratorspatial.py} +9 -10
- pyvale/{core/integratortype.py → integratortype.py} +7 -8
- pyvale/output.py +17 -0
- pyvale/pyvaleexceptions.py +11 -0
- pyvale/{core/raster.py → raster.py} +8 -8
- pyvale/{core/rastercy.py → rastercy.py} +11 -10
- pyvale/{core/rasternp.py → rasternp.py} +12 -13
- pyvale/{core/rendermesh.py → rendermesh.py} +10 -19
- pyvale/{core/sensorarray.py → sensorarray.py} +7 -8
- pyvale/{core/sensorarrayfactory.py → sensorarrayfactory.py} +64 -78
- pyvale/{core/sensorarraypoint.py → sensorarraypoint.py} +39 -41
- pyvale/{core/sensordata.py → sensordata.py} +7 -8
- pyvale/sensordescriptor.py +213 -0
- pyvale/{core/sensortools.py → sensortools.py} +8 -9
- pyvale/simcases/case00_HEX20.i +5 -5
- pyvale/simcases/case00_HEX27.i +5 -5
- pyvale/simcases/case00_HEX8.i +242 -0
- pyvale/simcases/case00_TET10.i +2 -2
- pyvale/simcases/case00_TET14.i +2 -2
- pyvale/simcases/case00_TET4.i +242 -0
- pyvale/simcases/run_1case.py +1 -1
- pyvale/simtools.py +67 -0
- pyvale/visualexpplotter.py +191 -0
- pyvale/{core/visualimagedef.py → visualimagedef.py} +13 -10
- pyvale/{core/visualimages.py → visualimages.py} +10 -9
- pyvale/visualopts.py +493 -0
- pyvale/{core/visualsimanimator.py → visualsimanimator.py} +47 -19
- pyvale/visualsimsensors.py +318 -0
- pyvale/visualtools.py +136 -0
- pyvale/visualtraceplotter.py +142 -0
- {pyvale-2025.4.0.dist-info → pyvale-2025.5.1.dist-info}/METADATA +17 -14
- pyvale-2025.5.1.dist-info/RECORD +172 -0
- {pyvale-2025.4.0.dist-info → pyvale-2025.5.1.dist-info}/WHEEL +1 -1
- pyvale/core/__init__.py +0 -7
- pyvale/core/analyticmeshgen.py +0 -59
- pyvale/core/analyticsimdatagenerator.py +0 -160
- pyvale/core/cython/rastercyth.c +0 -32267
- pyvale/core/experimentsimulator.py +0 -99
- pyvale/core/fieldconverter.py +0 -154
- pyvale/core/integratorrectangle.py +0 -88
- pyvale/core/optimcheckfuncs.py +0 -153
- pyvale/core/sensordescriptor.py +0 -101
- pyvale/core/visualexpplotter.py +0 -151
- pyvale/core/visualopts.py +0 -180
- pyvale/core/visualsimplotter.py +0 -182
- pyvale/core/visualtools.py +0 -81
- pyvale/core/visualtraceplotter.py +0 -256
- pyvale/examples/analyticdatagen/__init__.py +0 -7
- pyvale/examples/ex1_1_thermal2d.py +0 -89
- pyvale/examples/ex1_2_thermal2d.py +0 -111
- pyvale/examples/ex1_3_thermal2d.py +0 -113
- pyvale/examples/ex1_5_thermal2d.py +0 -105
- pyvale/examples/ex2_1_thermal3d .py +0 -87
- pyvale/examples/ex2_2_thermal3d.py +0 -51
- pyvale/examples/ex2_3_thermal3d.py +0 -109
- pyvale/examples/ex3_1_displacement2d.py +0 -47
- pyvale/examples/ex3_2_displacement2d.py +0 -79
- pyvale/examples/ex3_3_displacement2d.py +0 -104
- pyvale/examples/ex3_4_displacement2d.py +0 -105
- pyvale/examples/ex4_1_strain2d.py +0 -57
- pyvale/examples/ex4_2_strain2d.py +0 -79
- pyvale/examples/ex4_3_strain2d.py +0 -100
- pyvale/examples/ex5_1_multiphysics2d.py +0 -78
- pyvale/examples/ex6_1_multiphysics2d_expsim.py +0 -118
- pyvale/examples/ex6_2_multiphysics3d_expsim.py +0 -158
- pyvale/examples/features/__init__.py +0 -7
- 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-2025.4.0.dist-info/RECORD +0 -157
- {pyvale-2025.4.0.dist-info → pyvale-2025.5.1.dist-info}/licenses/LICENSE +0 -0
- {pyvale-2025.4.0.dist-info → pyvale-2025.5.1.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]}")
|
|
@@ -1,24 +1,28 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# pyvale: the python validation engine
|
|
3
|
+
# License: MIT
|
|
4
|
+
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
|
|
1
7
|
"""
|
|
2
|
-
|
|
3
|
-
pyvale: the python validation engine
|
|
4
|
-
License: MIT
|
|
5
|
-
Copyright (C) 2025 The Computer Aided Validation Team
|
|
6
|
-
================================================================================
|
|
8
|
+
NOTE: This module is a feature under developement.
|
|
7
9
|
"""
|
|
10
|
+
|
|
8
11
|
import warnings
|
|
9
12
|
from pathlib import Path
|
|
10
13
|
import numpy as np
|
|
11
14
|
from scipy.signal import convolve2d
|
|
15
|
+
import copy
|
|
12
16
|
from scipy.spatial.transform import Rotation
|
|
13
17
|
import matplotlib.image as mplim
|
|
14
18
|
from PIL import Image
|
|
15
|
-
from pyvale.
|
|
16
|
-
from pyvale.
|
|
19
|
+
from pyvale.cameradata2d import CameraData2D
|
|
20
|
+
from pyvale.sensordata import SensorData
|
|
21
|
+
from pyvale.cameradata import CameraData
|
|
22
|
+
from pyvale.camerastereo import CameraStereo
|
|
17
23
|
|
|
18
|
-
# NOTE: This module is a feature under developement.
|
|
19
24
|
|
|
20
25
|
class CameraTools:
|
|
21
|
-
#-------------------------------------------------------------------------------
|
|
22
26
|
@staticmethod
|
|
23
27
|
def load_image(im_path: Path) -> np.ndarray:
|
|
24
28
|
|
|
@@ -56,7 +60,6 @@ class CameraTools:
|
|
|
56
60
|
|
|
57
61
|
return num_str
|
|
58
62
|
|
|
59
|
-
#-------------------------------------------------------------------------------
|
|
60
63
|
@staticmethod
|
|
61
64
|
def pixel_vec_px(pixels_count: np.ndarray) -> tuple[np.ndarray,np.ndarray]:
|
|
62
65
|
px_vec_x = np.arange(0,pixels_count[0],1)
|
|
@@ -72,7 +75,6 @@ class CameraTools:
|
|
|
72
75
|
(px_grid_x,px_grid_y) = CameraTools.pixel_grid_px(pixels_count)
|
|
73
76
|
return (px_grid_x.flatten(),px_grid_y.flatten())
|
|
74
77
|
|
|
75
|
-
#-------------------------------------------------------------------------------
|
|
76
78
|
@staticmethod
|
|
77
79
|
def subpixel_vec_px(pixels_count: np.ndarray,
|
|
78
80
|
subsample: int = 2) -> tuple[np.ndarray,np.ndarray]:
|
|
@@ -92,7 +94,6 @@ class CameraTools:
|
|
|
92
94
|
(px_grid_x,px_grid_y) = CameraTools.subpixel_grid_px(pixels_count,subsample)
|
|
93
95
|
return (px_grid_x.flatten(),px_grid_y.flatten())
|
|
94
96
|
|
|
95
|
-
#-------------------------------------------------------------------------------
|
|
96
97
|
@staticmethod
|
|
97
98
|
def pixel_vec_leng(field_of_view: np.ndarray,
|
|
98
99
|
leng_per_px: float) -> tuple[np.ndarray,np.ndarray]:
|
|
@@ -116,7 +117,7 @@ class CameraTools:
|
|
|
116
117
|
(px_grid_x,px_grid_y) = CameraTools.pixel_grid_leng(field_of_view,leng_per_px)
|
|
117
118
|
return (px_grid_x.flatten(),px_grid_y.flatten())
|
|
118
119
|
|
|
119
|
-
|
|
120
|
+
|
|
120
121
|
@staticmethod
|
|
121
122
|
def subpixel_vec_leng(field_of_view: np.ndarray,
|
|
122
123
|
leng_per_px: float,
|
|
@@ -149,7 +150,6 @@ class CameraTools:
|
|
|
149
150
|
subsample)
|
|
150
151
|
return (px_grid_x.flatten(),px_grid_y.flatten())
|
|
151
152
|
|
|
152
|
-
#-------------------------------------------------------------------------------
|
|
153
153
|
@staticmethod
|
|
154
154
|
def calc_resolution_from_sim_2d(pixels_count: np.ndarray,
|
|
155
155
|
coords: np.ndarray,
|
|
@@ -211,7 +211,6 @@ class CameraTools:
|
|
|
211
211
|
round(subsample/2)-1::subsample]
|
|
212
212
|
return avg_image
|
|
213
213
|
|
|
214
|
-
#---------------------------------------------------------------------------
|
|
215
214
|
@staticmethod
|
|
216
215
|
def build_sensor_data_from_camera_2d(cam_data: CameraData2D) -> SensorData:
|
|
217
216
|
pixels_vectorised = CameraTools.vectorise_pixel_grid_leng(cam_data.field_of_view,
|
|
@@ -263,16 +262,16 @@ class CameraTools:
|
|
|
263
262
|
boundbox_cam_leng = (np.max(bound_box_cam_vecs,axis=1)
|
|
264
263
|
- np.min(bound_box_cam_vecs,axis=1))
|
|
265
264
|
|
|
266
|
-
print(80*"-")
|
|
267
|
-
print(f"{bb_min=}")
|
|
268
|
-
print(f"{bb_max=}")
|
|
269
|
-
print()
|
|
270
|
-
print("Cam to world mat:")
|
|
271
|
-
print(cam_to_world_mat)
|
|
272
|
-
print()
|
|
273
|
-
print("World to cam mat:")
|
|
274
|
-
print(world_to_cam_mat)
|
|
275
|
-
print(80*"-")
|
|
265
|
+
# print(80*"-")
|
|
266
|
+
# print(f"{bb_min=}")
|
|
267
|
+
# print(f"{bb_max=}")
|
|
268
|
+
# print()
|
|
269
|
+
# print("Cam to world mat:")
|
|
270
|
+
# print(cam_to_world_mat)
|
|
271
|
+
# print()
|
|
272
|
+
# print("World to cam mat:")
|
|
273
|
+
# print(world_to_cam_mat)
|
|
274
|
+
# print(80*"-")
|
|
276
275
|
|
|
277
276
|
return np.array((boundbox_cam_leng[xx],boundbox_cam_leng[yy]))
|
|
278
277
|
|
|
@@ -325,4 +324,199 @@ class CameraTools:
|
|
|
325
324
|
return (roi_pos_world,cam_pos_world)
|
|
326
325
|
|
|
327
326
|
|
|
328
|
-
|
|
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
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# pyvale: the python validation engine
|
|
3
|
+
# License: MIT
|
|
4
|
+
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
|
|
1
7
|
"""
|
|
2
|
-
|
|
3
|
-
pyvale: the python validation engine
|
|
4
|
-
License: MIT
|
|
5
|
-
Copyright (C) 2025 The Computer Aided Validation Team
|
|
6
|
-
================================================================================
|
|
8
|
+
NOTE: this module is a feature under developement.
|
|
7
9
|
"""
|
|
10
|
+
|
|
8
11
|
import numpy as np
|
|
9
12
|
import cython
|
|
10
13
|
from cython.parallel import prange, parallel, threadid
|
|
11
14
|
from cython.cimports.libc.math import floor, ceil
|
|
12
15
|
|
|
13
|
-
from pyvale.
|
|
14
|
-
from pyvale.
|
|
16
|
+
from pyvale.rendermesh import RenderMeshData
|
|
17
|
+
from pyvale.cameradata import CameraData
|
|
15
18
|
|
|
19
|
+
# NOTE: This module is a feature under developement.
|
|
16
20
|
|
|
17
21
|
@cython.nogil
|
|
18
22
|
@cython.cfunc # python+C or cython.cfunc for C only
|
pyvale/data/__init__.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
================================================================================
|
|
7
|
-
"""
|
|
1
|
+
#===============================================================================
|
|
2
|
+
# pyvale: the python validation engine
|
|
3
|
+
# License: MIT
|
|
4
|
+
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
+
#===============================================================================
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|