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/blenderscene.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# pyvale: the python validation engine
|
|
3
|
+
# License: MIT
|
|
4
|
+
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
import numpy as np
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import bpy
|
|
9
|
+
from pyvale.cameradata import CameraData
|
|
10
|
+
from pyvale.blenderlightdata import BlenderLightData
|
|
11
|
+
from pyvale.blendertools import BlenderTools
|
|
12
|
+
from pyvale.simtools import SimTools
|
|
13
|
+
from pyvale.blendermaterialdata import BlenderMaterialData
|
|
14
|
+
from pyvale.blenderrenderdata import RenderData, RenderEngine
|
|
15
|
+
from pyvale.camerastereo import CameraStereo
|
|
16
|
+
from pyvale.rendermesh import RenderMeshData
|
|
17
|
+
from pyvale.pyvaleexceptions import BlenderError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BlenderScene():
|
|
21
|
+
"""Namespace for creating a scene within Blender.
|
|
22
|
+
Methods include adding an object, camera, light and adding a speckle pattern,
|
|
23
|
+
as well as deforming the object, and then rendering the scene.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self.reset_scene()
|
|
28
|
+
|
|
29
|
+
def reset_scene(self) -> None:
|
|
30
|
+
"""This method creates a new, empty scene.
|
|
31
|
+
The units are then set to milimetres, and all nodes are cleared from the
|
|
32
|
+
scene. This method will be called when the class is initialised.
|
|
33
|
+
"""
|
|
34
|
+
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
35
|
+
|
|
36
|
+
bpy.context.scene.unit_settings.scale_length = 0.001
|
|
37
|
+
bpy.context.scene.unit_settings.length_unit = 'MILLIMETERS'
|
|
38
|
+
|
|
39
|
+
new_world = bpy.data.worlds.new('World')
|
|
40
|
+
bpy.context.scene.world = new_world
|
|
41
|
+
new_world.use_nodes = True
|
|
42
|
+
node_tree = new_world.node_tree
|
|
43
|
+
nodes = node_tree.nodes
|
|
44
|
+
|
|
45
|
+
nodes.clear()
|
|
46
|
+
bg_node = nodes.new(type='ShaderNodeBackground')
|
|
47
|
+
bg_node.inputs[0].default_value = [0.5, 0.5, 0.5, 1]
|
|
48
|
+
bg_node.inputs[1].default_value = 0
|
|
49
|
+
|
|
50
|
+
def add_camera(self, cam_data:CameraData) -> bpy.data.objects:
|
|
51
|
+
"""Method to add a camera object within Blender.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
cam_data : CameraData
|
|
56
|
+
A dataclass containing the necessary parameters to create the camera
|
|
57
|
+
object in Blender.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
bpy.data.objects
|
|
62
|
+
The Blender camera object that is created.
|
|
63
|
+
"""
|
|
64
|
+
new_cam = bpy.data.cameras.new('Camera')
|
|
65
|
+
camera = bpy.data.objects.new('Camera', new_cam)
|
|
66
|
+
bpy.context.collection.objects.link(camera)
|
|
67
|
+
|
|
68
|
+
camera.location = (cam_data.pos_world[0],
|
|
69
|
+
cam_data.pos_world[1],
|
|
70
|
+
cam_data.pos_world[2])
|
|
71
|
+
camera.rotation_mode = 'XYZ'
|
|
72
|
+
rotation_euler = cam_data.rot_world.as_euler("xyz", degrees=False)
|
|
73
|
+
camera.rotation_euler = rotation_euler
|
|
74
|
+
|
|
75
|
+
pixels_num = (int(cam_data.pixels_num[0]), int(cam_data.pixels_num[1]))
|
|
76
|
+
camera['sensor_px'] = pixels_num
|
|
77
|
+
camera['px_size'] = cam_data.pixels_size
|
|
78
|
+
camera['k1'] = cam_data.k1
|
|
79
|
+
camera['k2'] = cam_data.k2
|
|
80
|
+
camera['k3'] = cam_data.k3
|
|
81
|
+
camera['p1'] = cam_data.p1
|
|
82
|
+
camera['p2'] = cam_data.p2
|
|
83
|
+
camera['c0'] = cam_data.c0
|
|
84
|
+
camera['c1'] = cam_data.c1
|
|
85
|
+
|
|
86
|
+
new_cam.lens_unit = 'MILLIMETERS'
|
|
87
|
+
new_cam.lens = cam_data.focal_length
|
|
88
|
+
new_cam.sensor_fit = 'HORIZONTAL'
|
|
89
|
+
new_cam.sensor_width = cam_data.sensor_size[0]
|
|
90
|
+
new_cam.sensor_height = cam_data.sensor_size[1]
|
|
91
|
+
|
|
92
|
+
if cam_data.fstop is not None:
|
|
93
|
+
new_cam.dof.focus_distance = cam_data.image_dist
|
|
94
|
+
new_cam.dof.use_dof = True
|
|
95
|
+
new_cam.dof.aperture_fstop = cam_data.fstop
|
|
96
|
+
|
|
97
|
+
bpy.context.scene.camera = camera
|
|
98
|
+
return camera
|
|
99
|
+
|
|
100
|
+
def add_stereo_system(self, stereo_system: CameraStereo) -> tuple[bpy.data.objects,
|
|
101
|
+
bpy.data.objects]:
|
|
102
|
+
# TODO: Correct docstring
|
|
103
|
+
"""A method to add a stereo camera system within Blender, given an
|
|
104
|
+
instance of the CameraStereo class (that describes a stereo system).
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
stereo_system: CameraStereo
|
|
109
|
+
An instance of the CameraStereo class, describing a stereo system.
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
tuple[bpy.data.objects, bpy.data.objects]
|
|
114
|
+
A tuple of the Blender camera objects: camera 0 and camera 1.
|
|
115
|
+
"""
|
|
116
|
+
cam0 = self.add_camera(stereo_system.cam_data_0)
|
|
117
|
+
cam1 = self.add_camera(stereo_system.cam_data_1)
|
|
118
|
+
return cam0, cam1
|
|
119
|
+
|
|
120
|
+
def add_light(self, light_data: BlenderLightData) -> bpy.data.objects:
|
|
121
|
+
"""A method to add a light object within Blender.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
light_data : BlenderLightData
|
|
126
|
+
A dataclass contain the necessary parameters to create a Blender
|
|
127
|
+
light object.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
bpy.data.objects
|
|
132
|
+
The Blender light object that is created.
|
|
133
|
+
"""
|
|
134
|
+
type = light_data.type.value
|
|
135
|
+
name = type.capitalize() + 'Light'
|
|
136
|
+
light = bpy.data.lights.new(name=name, type=type)
|
|
137
|
+
light_ob = bpy.data.objects.new(name=name, object_data=light)
|
|
138
|
+
|
|
139
|
+
light_ob.location = (light_data.pos_world[0],
|
|
140
|
+
light_data.pos_world[1],
|
|
141
|
+
light_data.pos_world[2])
|
|
142
|
+
|
|
143
|
+
light_ob.rotation_mode = 'XYZ'
|
|
144
|
+
rotation_euler = light_data.rot_world.as_euler("xyz", degrees=False)
|
|
145
|
+
light_ob.rotation_euler = rotation_euler
|
|
146
|
+
|
|
147
|
+
light.energy = light_data.energy * 10**6
|
|
148
|
+
light.shadow_soft_size = light_data.shadow_soft_size
|
|
149
|
+
|
|
150
|
+
bpy.context.collection.objects.link(light_ob)
|
|
151
|
+
|
|
152
|
+
return light_ob
|
|
153
|
+
|
|
154
|
+
def add_part(self,
|
|
155
|
+
render_mesh: RenderMeshData,
|
|
156
|
+
sim_spat_dim: int) -> bpy.data.objects:
|
|
157
|
+
"""A method to add a part mesh into Blender, given a RenderMeshData object.
|
|
158
|
+
This is done by taking the mesh information from the RenderMeshData
|
|
159
|
+
object and converting it into a form that is accepted by Blender. It
|
|
160
|
+
should be noted that the object is placed at the origin and centred
|
|
161
|
+
around its geometric centroid.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
render_mesh: RenderMeshData
|
|
166
|
+
A dataclass containing the mesh information of the skinned
|
|
167
|
+
simulation mesh.
|
|
168
|
+
sim_spat_dim: int
|
|
169
|
+
The spatial dimension of the simulation mesh.
|
|
170
|
+
|
|
171
|
+
Returns
|
|
172
|
+
-------
|
|
173
|
+
bpy.data.objects
|
|
174
|
+
The Blender part object that is created.
|
|
175
|
+
"""
|
|
176
|
+
nodes_centred = SimTools.centre_mesh_nodes(render_mesh.coords,
|
|
177
|
+
sim_spat_dim)
|
|
178
|
+
vertices = np.delete(nodes_centred, 3, axis=1)
|
|
179
|
+
faces = render_mesh.connectivity
|
|
180
|
+
|
|
181
|
+
mesh = bpy.data.meshes.new("Part")
|
|
182
|
+
mesh.from_pydata(vertices, [], faces)
|
|
183
|
+
part = bpy.data.objects.new("Part", mesh)
|
|
184
|
+
|
|
185
|
+
bpy.context.scene.collection.objects.link(part)
|
|
186
|
+
|
|
187
|
+
return part
|
|
188
|
+
|
|
189
|
+
def add_cal_target(self, target_size: np.ndarray) -> bpy.data.objects:
|
|
190
|
+
"""A function to add a calibration target object to a Blender scene.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
target_size : np.ndarray
|
|
195
|
+
The dimensions of the calibration target, with the
|
|
196
|
+
shape=(width, height, depth).
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
bpy.data.objects
|
|
201
|
+
A Blender part object of the calibration target.
|
|
202
|
+
"""
|
|
203
|
+
nodes = [
|
|
204
|
+
(-target_size[0] / 2, target_size[1] / 2, 0),
|
|
205
|
+
(-target_size[0] / 2, -target_size[1] / 2, 0),
|
|
206
|
+
(target_size[0] / 2, -target_size[1] / 2, 0),
|
|
207
|
+
(target_size[0] / 2, target_size[1] / 2, 0),
|
|
208
|
+
]
|
|
209
|
+
elements = [(0, 1, 2, 3)]
|
|
210
|
+
thickness = target_size[2]
|
|
211
|
+
mesh = bpy.data.meshes.new("part")
|
|
212
|
+
mesh.from_pydata(nodes, [], elements)
|
|
213
|
+
target = bpy.data.objects.new("specimen", mesh)
|
|
214
|
+
bpy.context.scene.collection.objects.link(target)
|
|
215
|
+
target.modifiers.new(name="solidify", type="SOLIDIFY")
|
|
216
|
+
target.modifiers["solidify"].thickness = thickness
|
|
217
|
+
target.location = (0, 0, -target_size[2])
|
|
218
|
+
return target
|
|
219
|
+
|
|
220
|
+
def add_speckle(self,
|
|
221
|
+
part: bpy.data.objects,
|
|
222
|
+
speckle_path: Path | None,
|
|
223
|
+
mat_data: BlenderMaterialData | None,
|
|
224
|
+
mm_px_resolution: float,
|
|
225
|
+
cal: bool = False) -> None:
|
|
226
|
+
"""A method to add a speckle pattern to an existing mesh object within
|
|
227
|
+
Blender. The speckle pattern can either be passed in as an image file
|
|
228
|
+
that is saved to the disc, or can be generated dynamically (this is
|
|
229
|
+
currently not an option but this method has the capaibility to link up
|
|
230
|
+
to a speckle pattern generator)
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
part : bpy.data.objects
|
|
235
|
+
The Blender part object, to which the speckle is to be applied.
|
|
236
|
+
speckle_path : Path | None
|
|
237
|
+
The filepath containing the speckle pattern image. If this is None,
|
|
238
|
+
there will be capability to generate a speckle pattern.
|
|
239
|
+
mat_data : BlenderMaterialData | None
|
|
240
|
+
A dataclass containin the material parameters. If this is None, it
|
|
241
|
+
is initialised within the method.
|
|
242
|
+
mm_px_resolution: float
|
|
243
|
+
The mm/px resolution of the camera. This is required in order to
|
|
244
|
+
scale the speckle image.
|
|
245
|
+
cal : bool, optional
|
|
246
|
+
A flag that can be set if a calibration target image is added to
|
|
247
|
+
a Blender part object. When set to True, the part object is UV
|
|
248
|
+
unwrapped differently to ensure the correct scaling, by default False
|
|
249
|
+
"""
|
|
250
|
+
BlenderTools.clear_material_nodes(part)
|
|
251
|
+
if mat_data is None:
|
|
252
|
+
mat_data = BlenderMaterialData()
|
|
253
|
+
if speckle_path.exists():
|
|
254
|
+
BlenderTools.add_image_texture(mat_data=mat_data, image_path=speckle_path)
|
|
255
|
+
else:
|
|
256
|
+
speckle_pattern = np.array() # Generate speckle pattern array
|
|
257
|
+
BlenderTools.add_image_texture(mat_data=mat_data, image_array=speckle_pattern)
|
|
258
|
+
BlenderTools.uv_unwrap_part(part, mm_px_resolution, cal)
|
|
259
|
+
|
|
260
|
+
def _debug_deform(self,
|
|
261
|
+
render_mesh: RenderMeshData,
|
|
262
|
+
sim_spat_dim:int,
|
|
263
|
+
part: bpy.data.objects) -> None:
|
|
264
|
+
"""A method to deform the Blender mesh object using the simulation results.
|
|
265
|
+
This is done by taking the displacements to the nodes, and applying it
|
|
266
|
+
in Blender. It should be noted that this only deforms the mesh without
|
|
267
|
+
rendering any images, mainly useful for debugging code.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
sim_data : mh.SimData
|
|
272
|
+
A dataclass containing the simulation information i.e. the displacements
|
|
273
|
+
to all the nodes in the mesh.
|
|
274
|
+
part : bpy.data.objects
|
|
275
|
+
The Blender part object which is to be deformed, normally as sample
|
|
276
|
+
object.
|
|
277
|
+
"""
|
|
278
|
+
render_mesh.coords = SimTools.centre_mesh_nodes(render_mesh.coords,
|
|
279
|
+
sim_spat_dim)
|
|
280
|
+
timesteps = render_mesh.fields_render.shape[1]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
for timestep in range(1, timesteps):
|
|
284
|
+
deformed_nodes = SimTools.get_deformed_nodes(timestep,
|
|
285
|
+
render_mesh)
|
|
286
|
+
if deformed_nodes is not None:
|
|
287
|
+
BlenderTools.deform_single_timestep(part, deformed_nodes)
|
|
288
|
+
BlenderTools.set_new_frame(part)
|
|
289
|
+
|
|
290
|
+
def render_single_image(self,
|
|
291
|
+
render_data: RenderData,
|
|
292
|
+
stage_image: bool | None = True) -> None | np.ndarray:
|
|
293
|
+
"""A method to render an images(s) of the current scene in Blender.
|
|
294
|
+
Depending on the number of cameras, either one or two images will be
|
|
295
|
+
rendered.
|
|
296
|
+
|
|
297
|
+
Parameters
|
|
298
|
+
----------
|
|
299
|
+
render_data : RenderData
|
|
300
|
+
A dataclass containing the parameters needed to render an image.
|
|
301
|
+
stage_image : bool | None, optional
|
|
302
|
+
A flag that can be set to either save the rendered to disk or not.
|
|
303
|
+
If set to False, an array of the image or stack of images will be
|
|
304
|
+
returned, by default True. In order to output these images as an
|
|
305
|
+
array, the image will first be saved to the disk and then bounced
|
|
306
|
+
back as an array.
|
|
307
|
+
|
|
308
|
+
Returns
|
|
309
|
+
-------
|
|
310
|
+
None | np.ndarray
|
|
311
|
+
Nothing is returned if the image(s) is saved to disk (when save set
|
|
312
|
+
to True). When save is set to False, the image array is returned.
|
|
313
|
+
For a 2D system, an array with shape=(pixels_num_y, pixels_num_x) is
|
|
314
|
+
returned. For a 3D system, a stack of arrays with
|
|
315
|
+
shape=(pixels_num_y, pixels_num_x, 2) is returned.
|
|
316
|
+
"""
|
|
317
|
+
bpy.context.scene.render.engine = render_data.engine.value
|
|
318
|
+
bpy.context.scene.render.image_settings.color_mode = "BW"
|
|
319
|
+
bpy.context.scene.render.image_settings.color_depth = str(render_data.bit_size)
|
|
320
|
+
bpy.context.scene.render.threads_mode = "FIXED"
|
|
321
|
+
bpy.context.scene.render.threads = render_data.threads
|
|
322
|
+
bpy.context.scene.render.image_settings.file_format = "TIFF"
|
|
323
|
+
|
|
324
|
+
if render_data.engine == RenderEngine.CYCLES:
|
|
325
|
+
bpy.context.scene.cycles.samples = render_data.samples
|
|
326
|
+
bpy.context.scene.cycles.max_bounces = render_data.max_bounces
|
|
327
|
+
elif render_data.engine == RenderEngine.EEVEE:
|
|
328
|
+
bpy.context.scene.eevee.taa_render_samples = render_data.samples
|
|
329
|
+
|
|
330
|
+
if not render_data.base_dir.is_dir():
|
|
331
|
+
raise BlenderError("The specified save directory does not exist")
|
|
332
|
+
|
|
333
|
+
save_dir = render_data.base_dir / "blenderimages"
|
|
334
|
+
if not save_dir.is_dir():
|
|
335
|
+
save_dir.mkdir(parents=True, exist_ok=True)
|
|
336
|
+
|
|
337
|
+
if isinstance(render_data.cam_data, tuple):
|
|
338
|
+
cam_count = 0
|
|
339
|
+
image_count = 0
|
|
340
|
+
image_arrays = []
|
|
341
|
+
for cam in [obj for obj in bpy.data.objects if obj.type == "CAMERA"]:
|
|
342
|
+
bpy.context.scene.camera = cam
|
|
343
|
+
cam_data_render = render_data.cam_data[cam_count]
|
|
344
|
+
bpy.context.scene.render.resolution_x = cam_data_render.pixels_num[0]
|
|
345
|
+
bpy.context.scene.render.resolution_y = cam_data_render.pixels_num[1]
|
|
346
|
+
filename = "blenderimage_" + str(image_count) + "_" + str(cam_count) + ".tiff"
|
|
347
|
+
filepath = save_dir / filename
|
|
348
|
+
bpy.context.scene.render.filepath = str(filepath)
|
|
349
|
+
if stage_image:
|
|
350
|
+
bpy.ops.render.render(write_still=True)
|
|
351
|
+
image_array = BlenderTools.save_render_as_array(filepath)
|
|
352
|
+
image_arrays.append(image_array)
|
|
353
|
+
else:
|
|
354
|
+
bpy.ops.render.render(write_still=True)
|
|
355
|
+
cam_count += 1
|
|
356
|
+
if stage_image:
|
|
357
|
+
image_arrays = np.dstack(image_arrays)
|
|
358
|
+
return image_arrays
|
|
359
|
+
else:
|
|
360
|
+
image_count = 0
|
|
361
|
+
bpy.context.scene.render.resolution_x = render_data.cam_data.pixels_num[0]
|
|
362
|
+
bpy.context.scene.render.resolution_y = render_data.cam_data.pixels_num[1]
|
|
363
|
+
filename = "blenderimage_" + str(image_count) + ".tiff"
|
|
364
|
+
filepath = save_dir / filename
|
|
365
|
+
bpy.context.scene.render.filepath = str(filepath)
|
|
366
|
+
if stage_image:
|
|
367
|
+
bpy.ops.render.render(write_still=True)
|
|
368
|
+
image_array = BlenderTools.save_render_as_array(filepath)
|
|
369
|
+
return image_array
|
|
370
|
+
else:
|
|
371
|
+
bpy.ops.render.render(write_still=True)
|
|
372
|
+
|
|
373
|
+
def render_deformed_images(self,
|
|
374
|
+
render_mesh: RenderMeshData,
|
|
375
|
+
sim_spat_dim: int,
|
|
376
|
+
render_data:RenderData,
|
|
377
|
+
part: bpy.data.objects,
|
|
378
|
+
stage_image: bool | None = True) -> None | np.ndarray:
|
|
379
|
+
"""A method to deform the mesh object at all timesteps, and render
|
|
380
|
+
image(s) at each timestep
|
|
381
|
+
|
|
382
|
+
Parameters
|
|
383
|
+
----------
|
|
384
|
+
render_mesh : RenderMeshData
|
|
385
|
+
A dataclass containing the skimmed mesh and simulation information
|
|
386
|
+
needed to deform the sample.
|
|
387
|
+
sim_spat_dim: int
|
|
388
|
+
The spatial dimension of the simulation.
|
|
389
|
+
render_data : RenderData
|
|
390
|
+
A dataclass containing the parameters necessary to render an image.
|
|
391
|
+
part : bpy.data.objects
|
|
392
|
+
The Blender part object to be deformed.
|
|
393
|
+
stage_image : bool | None, optional
|
|
394
|
+
A flag that can be set to save the rendered image to disk or not,
|
|
395
|
+
by default True. In order to output these images as an
|
|
396
|
+
array, the image will first be saved to the disk and then bounced
|
|
397
|
+
back as an array.
|
|
398
|
+
|
|
399
|
+
Returns
|
|
400
|
+
-------
|
|
401
|
+
None | np.ndarray
|
|
402
|
+
Either nothing is returned if the image is saved
|
|
403
|
+
to disk or a stack of image arrays are returned with the following
|
|
404
|
+
dimensions: shape=(pixels_num_y, pixels_num_x, (num_timesteps + 1)
|
|
405
|
+
for 2D setups and shape=(pixels_num_y, pixels_num_x, (num_timesteps + 1)*2)
|
|
406
|
+
for 3D setups. The additional image is the reference image. For
|
|
407
|
+
3D setups, the images in the stack alternate between camera 0 and
|
|
408
|
+
camera 1.
|
|
409
|
+
"""
|
|
410
|
+
render_mesh.coords = SimTools.centre_mesh_nodes(render_mesh.coords,
|
|
411
|
+
sim_spat_dim)
|
|
412
|
+
timesteps = render_mesh.fields_render.shape[1]
|
|
413
|
+
|
|
414
|
+
# Render parameters
|
|
415
|
+
bpy.context.scene.render.engine = render_data.engine.value
|
|
416
|
+
bpy.context.scene.render.image_settings.color_mode = "BW"
|
|
417
|
+
bpy.context.scene.render.image_settings.color_depth = str(render_data.bit_size)
|
|
418
|
+
bpy.context.scene.render.threads_mode = "FIXED"
|
|
419
|
+
bpy.context.scene.render.threads = render_data.threads
|
|
420
|
+
bpy.context.scene.render.image_settings.file_format = "TIFF"
|
|
421
|
+
|
|
422
|
+
if render_data.engine == RenderEngine.CYCLES:
|
|
423
|
+
bpy.context.scene.cycles.samples = render_data.samples
|
|
424
|
+
bpy.context.scene.cycles.max_bounces = render_data.max_bounces
|
|
425
|
+
elif render_data.engine == RenderEngine.EEVEE:
|
|
426
|
+
bpy.context.scene.eevee.taa_render_samples = render_data.samples
|
|
427
|
+
|
|
428
|
+
if not render_data.base_dir.is_dir():
|
|
429
|
+
raise BlenderError("The specified save directory does not exist")
|
|
430
|
+
|
|
431
|
+
save_dir = render_data.base_dir / "blenderimages"
|
|
432
|
+
if not save_dir.is_dir():
|
|
433
|
+
save_dir.mkdir(parents=True, exist_ok=True)
|
|
434
|
+
|
|
435
|
+
image_arrays = []
|
|
436
|
+
for timestep in range(0, timesteps):
|
|
437
|
+
deformed_nodes = SimTools.get_deformed_nodes(timestep,
|
|
438
|
+
render_mesh)
|
|
439
|
+
if deformed_nodes is not None:
|
|
440
|
+
BlenderTools.deform_single_timestep(part, deformed_nodes)
|
|
441
|
+
BlenderTools.set_new_frame(part)
|
|
442
|
+
|
|
443
|
+
if isinstance(render_data.cam_data, tuple):
|
|
444
|
+
cam_count = 0
|
|
445
|
+
for cam in [obj for obj in bpy.data.objects if obj.type == "CAMERA"]:
|
|
446
|
+
bpy.context.scene.camera = cam
|
|
447
|
+
cam_data_render = render_data.cam_data[cam_count]
|
|
448
|
+
bpy.context.scene.render.resolution_x = cam_data_render.pixels_num[0]
|
|
449
|
+
bpy.context.scene.render.resolution_y = cam_data_render.pixels_num[1]
|
|
450
|
+
filename = "blenderimage_" + str(timestep) + "_" + str(cam_count) + ".tiff"
|
|
451
|
+
filepath = save_dir / filename
|
|
452
|
+
bpy.context.scene.render.filepath = str(filepath)
|
|
453
|
+
if stage_image:
|
|
454
|
+
bpy.ops.render.render(write_still=True)
|
|
455
|
+
image_array = BlenderTools.save_render_as_array(filepath)
|
|
456
|
+
image_arrays.append(image_array)
|
|
457
|
+
else:
|
|
458
|
+
bpy.ops.render.render(write_still=True)
|
|
459
|
+
cam_count += 1
|
|
460
|
+
else:
|
|
461
|
+
bpy.context.scene.render.resolution_x = render_data.cam_data.pixels_num[0]
|
|
462
|
+
bpy.context.scene.render.resolution_y = render_data.cam_data.pixels_num[1]
|
|
463
|
+
filename = "blenderimage_" + str(timestep) + ".tiff"
|
|
464
|
+
filepath = save_dir / filename
|
|
465
|
+
bpy.context.scene.render.filepath = str(filepath)
|
|
466
|
+
if stage_image:
|
|
467
|
+
bpy.ops.render.render(write_still=True)
|
|
468
|
+
image_array = BlenderTools.save_render_as_array(filepath)
|
|
469
|
+
image_arrays.append(image_array)
|
|
470
|
+
else:
|
|
471
|
+
bpy.ops.render.render(write_still=True)
|
|
472
|
+
if stage_image:
|
|
473
|
+
image_arrays = np.dstack(image_arrays)
|
|
474
|
+
# TODO: Potentially change the way images are stacked for stereo systems
|
|
475
|
+
# Change it so it suits Joel's code
|
|
476
|
+
return image_arrays
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
|