pyvale 2025.5.3__cp311-cp311-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (174) hide show
  1. pyvale/__init__.py +89 -0
  2. pyvale/analyticmeshgen.py +102 -0
  3. pyvale/analyticsimdatafactory.py +91 -0
  4. pyvale/analyticsimdatagenerator.py +323 -0
  5. pyvale/blendercalibrationdata.py +15 -0
  6. pyvale/blenderlightdata.py +26 -0
  7. pyvale/blendermaterialdata.py +15 -0
  8. pyvale/blenderrenderdata.py +30 -0
  9. pyvale/blenderscene.py +488 -0
  10. pyvale/blendertools.py +420 -0
  11. pyvale/camera.py +146 -0
  12. pyvale/cameradata.py +69 -0
  13. pyvale/cameradata2d.py +84 -0
  14. pyvale/camerastereo.py +217 -0
  15. pyvale/cameratools.py +522 -0
  16. pyvale/cython/rastercyth.c +32211 -0
  17. pyvale/cython/rastercyth.cp311-win_amd64.pyd +0 -0
  18. pyvale/cython/rastercyth.py +640 -0
  19. pyvale/data/__init__.py +5 -0
  20. pyvale/data/cal_target.tiff +0 -0
  21. pyvale/data/case00_HEX20_out.e +0 -0
  22. pyvale/data/case00_HEX27_out.e +0 -0
  23. pyvale/data/case00_HEX8_out.e +0 -0
  24. pyvale/data/case00_TET10_out.e +0 -0
  25. pyvale/data/case00_TET14_out.e +0 -0
  26. pyvale/data/case00_TET4_out.e +0 -0
  27. pyvale/data/case13_out.e +0 -0
  28. pyvale/data/case16_out.e +0 -0
  29. pyvale/data/case17_out.e +0 -0
  30. pyvale/data/case18_1_out.e +0 -0
  31. pyvale/data/case18_2_out.e +0 -0
  32. pyvale/data/case18_3_out.e +0 -0
  33. pyvale/data/case25_out.e +0 -0
  34. pyvale/data/case26_out.e +0 -0
  35. pyvale/data/optspeckle_2464x2056px_spec5px_8bit_gblur1px.tiff +0 -0
  36. pyvale/dataset.py +325 -0
  37. pyvale/errorcalculator.py +109 -0
  38. pyvale/errordriftcalc.py +146 -0
  39. pyvale/errorintegrator.py +336 -0
  40. pyvale/errorrand.py +607 -0
  41. pyvale/errorsyscalib.py +134 -0
  42. pyvale/errorsysdep.py +327 -0
  43. pyvale/errorsysfield.py +414 -0
  44. pyvale/errorsysindep.py +808 -0
  45. pyvale/examples/__init__.py +5 -0
  46. pyvale/examples/basics/ex1_1_basicscalars_therm2d.py +131 -0
  47. pyvale/examples/basics/ex1_2_sensormodel_therm2d.py +158 -0
  48. pyvale/examples/basics/ex1_3_customsens_therm3d.py +216 -0
  49. pyvale/examples/basics/ex1_4_basicerrors_therm3d.py +153 -0
  50. pyvale/examples/basics/ex1_5_fielderrs_therm3d.py +168 -0
  51. pyvale/examples/basics/ex1_6_caliberrs_therm2d.py +133 -0
  52. pyvale/examples/basics/ex1_7_spatavg_therm2d.py +123 -0
  53. pyvale/examples/basics/ex2_1_basicvectors_disp2d.py +112 -0
  54. pyvale/examples/basics/ex2_2_vectorsens_disp2d.py +111 -0
  55. pyvale/examples/basics/ex2_3_sensangle_disp2d.py +139 -0
  56. pyvale/examples/basics/ex2_4_chainfielderrs_disp2d.py +196 -0
  57. pyvale/examples/basics/ex2_5_vectorfields3d_disp3d.py +109 -0
  58. pyvale/examples/basics/ex3_1_basictensors_strain2d.py +114 -0
  59. pyvale/examples/basics/ex3_2_tensorsens2d_strain2d.py +111 -0
  60. pyvale/examples/basics/ex3_3_tensorsens3d_strain3d.py +182 -0
  61. pyvale/examples/basics/ex4_1_expsim2d_thermmech2d.py +171 -0
  62. pyvale/examples/basics/ex4_2_expsim3d_thermmech3d.py +252 -0
  63. pyvale/examples/genanalyticdata/ex1_1_scalarvisualisation.py +35 -0
  64. pyvale/examples/genanalyticdata/ex1_2_scalarcasebuild.py +43 -0
  65. pyvale/examples/genanalyticdata/ex2_1_analyticsensors.py +80 -0
  66. pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +79 -0
  67. pyvale/examples/renderblender/ex1_1_blenderscene.py +121 -0
  68. pyvale/examples/renderblender/ex1_2_blenderdeformed.py +119 -0
  69. pyvale/examples/renderblender/ex2_1_stereoscene.py +128 -0
  70. pyvale/examples/renderblender/ex2_2_stereodeformed.py +131 -0
  71. pyvale/examples/renderblender/ex3_1_blendercalibration.py +120 -0
  72. pyvale/examples/renderrasterisation/ex_rastenp.py +153 -0
  73. pyvale/examples/renderrasterisation/ex_rastercyth_oneframe.py +218 -0
  74. pyvale/examples/renderrasterisation/ex_rastercyth_static_cypara.py +187 -0
  75. pyvale/examples/renderrasterisation/ex_rastercyth_static_pypara.py +190 -0
  76. pyvale/examples/visualisation/ex1_1_plot_traces.py +102 -0
  77. pyvale/examples/visualisation/ex2_1_animate_sim.py +89 -0
  78. pyvale/experimentsimulator.py +175 -0
  79. pyvale/field.py +128 -0
  80. pyvale/fieldconverter.py +351 -0
  81. pyvale/fieldsampler.py +111 -0
  82. pyvale/fieldscalar.py +166 -0
  83. pyvale/fieldtensor.py +218 -0
  84. pyvale/fieldtransform.py +388 -0
  85. pyvale/fieldvector.py +213 -0
  86. pyvale/generatorsrandom.py +505 -0
  87. pyvale/imagedef2d.py +569 -0
  88. pyvale/integratorfactory.py +240 -0
  89. pyvale/integratorquadrature.py +217 -0
  90. pyvale/integratorrectangle.py +165 -0
  91. pyvale/integratorspatial.py +89 -0
  92. pyvale/integratortype.py +43 -0
  93. pyvale/output.py +17 -0
  94. pyvale/pyvaleexceptions.py +11 -0
  95. pyvale/raster.py +31 -0
  96. pyvale/rastercy.py +77 -0
  97. pyvale/rasternp.py +603 -0
  98. pyvale/rendermesh.py +147 -0
  99. pyvale/sensorarray.py +178 -0
  100. pyvale/sensorarrayfactory.py +196 -0
  101. pyvale/sensorarraypoint.py +278 -0
  102. pyvale/sensordata.py +71 -0
  103. pyvale/sensordescriptor.py +213 -0
  104. pyvale/sensortools.py +142 -0
  105. pyvale/simcases/case00_HEX20.i +242 -0
  106. pyvale/simcases/case00_HEX27.i +242 -0
  107. pyvale/simcases/case00_HEX8.i +242 -0
  108. pyvale/simcases/case00_TET10.i +242 -0
  109. pyvale/simcases/case00_TET14.i +242 -0
  110. pyvale/simcases/case00_TET4.i +242 -0
  111. pyvale/simcases/case01.i +101 -0
  112. pyvale/simcases/case02.i +156 -0
  113. pyvale/simcases/case03.i +136 -0
  114. pyvale/simcases/case04.i +181 -0
  115. pyvale/simcases/case05.i +234 -0
  116. pyvale/simcases/case06.i +305 -0
  117. pyvale/simcases/case07.geo +135 -0
  118. pyvale/simcases/case07.i +87 -0
  119. pyvale/simcases/case08.geo +144 -0
  120. pyvale/simcases/case08.i +153 -0
  121. pyvale/simcases/case09.geo +204 -0
  122. pyvale/simcases/case09.i +87 -0
  123. pyvale/simcases/case10.geo +204 -0
  124. pyvale/simcases/case10.i +257 -0
  125. pyvale/simcases/case11.geo +337 -0
  126. pyvale/simcases/case11.i +147 -0
  127. pyvale/simcases/case12.geo +388 -0
  128. pyvale/simcases/case12.i +329 -0
  129. pyvale/simcases/case13.i +140 -0
  130. pyvale/simcases/case14.i +159 -0
  131. pyvale/simcases/case15.geo +337 -0
  132. pyvale/simcases/case15.i +150 -0
  133. pyvale/simcases/case16.geo +391 -0
  134. pyvale/simcases/case16.i +357 -0
  135. pyvale/simcases/case17.geo +135 -0
  136. pyvale/simcases/case17.i +144 -0
  137. pyvale/simcases/case18.i +254 -0
  138. pyvale/simcases/case18_1.i +254 -0
  139. pyvale/simcases/case18_2.i +254 -0
  140. pyvale/simcases/case18_3.i +254 -0
  141. pyvale/simcases/case19.geo +252 -0
  142. pyvale/simcases/case19.i +99 -0
  143. pyvale/simcases/case20.geo +252 -0
  144. pyvale/simcases/case20.i +250 -0
  145. pyvale/simcases/case21.geo +74 -0
  146. pyvale/simcases/case21.i +155 -0
  147. pyvale/simcases/case22.geo +82 -0
  148. pyvale/simcases/case22.i +140 -0
  149. pyvale/simcases/case23.geo +164 -0
  150. pyvale/simcases/case23.i +140 -0
  151. pyvale/simcases/case24.geo +79 -0
  152. pyvale/simcases/case24.i +123 -0
  153. pyvale/simcases/case25.geo +82 -0
  154. pyvale/simcases/case25.i +140 -0
  155. pyvale/simcases/case26.geo +166 -0
  156. pyvale/simcases/case26.i +140 -0
  157. pyvale/simcases/run_1case.py +61 -0
  158. pyvale/simcases/run_all_cases.py +69 -0
  159. pyvale/simcases/run_build_case.py +64 -0
  160. pyvale/simcases/run_example_cases.py +69 -0
  161. pyvale/simtools.py +67 -0
  162. pyvale/visualexpplotter.py +191 -0
  163. pyvale/visualimagedef.py +74 -0
  164. pyvale/visualimages.py +76 -0
  165. pyvale/visualopts.py +493 -0
  166. pyvale/visualsimanimator.py +111 -0
  167. pyvale/visualsimsensors.py +318 -0
  168. pyvale/visualtools.py +136 -0
  169. pyvale/visualtraceplotter.py +142 -0
  170. pyvale-2025.5.3.dist-info/METADATA +144 -0
  171. pyvale-2025.5.3.dist-info/RECORD +174 -0
  172. pyvale-2025.5.3.dist-info/WHEEL +5 -0
  173. pyvale-2025.5.3.dist-info/licenses/LICENSE +21 -0
  174. pyvale-2025.5.3.dist-info/top_level.txt +1 -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
+