lavavu 1.8.84__cp312-cp312-macosx_11_0_arm64.whl → 1.9.5__cp312-cp312-macosx_11_0_arm64.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.
- lavavu/_LavaVuPython.cpython-312-darwin.so +0 -0
- lavavu/control.py +18 -5
- lavavu/html/webview.html +1 -1
- lavavu/lavavu.py +391 -116
- lavavu/osmesa/LavaVuPython.py +561 -0
- lavavu/osmesa/__init__.py +0 -0
- lavavu/shaders/default.frag +0 -6
- lavavu/shaders/default.vert +4 -4
- lavavu/shaders/fontShader.frag +0 -5
- lavavu/shaders/lineShader.frag +0 -4
- lavavu/shaders/lineShader.vert +0 -2
- lavavu/shaders/pointShader.frag +0 -5
- lavavu/shaders/pointShader.vert +0 -4
- lavavu/shaders/triShader.frag +0 -17
- lavavu/shaders/triShader.vert +0 -4
- lavavu/shaders/volumeShader.frag +0 -63
- lavavu/tracers.py +180 -110
- {lavavu-1.8.84.dist-info → lavavu-1.9.5.dist-info}/METADATA +22 -17
- {lavavu-1.8.84.dist-info → lavavu-1.9.5.dist-info}/RECORD +23 -21
- {lavavu-1.8.84.dist-info → lavavu-1.9.5.dist-info}/WHEEL +2 -1
- {lavavu-1.8.84.dist-info → lavavu-1.9.5.dist-info}/entry_points.txt +0 -0
- {lavavu-1.8.84.dist-info → lavavu-1.9.5.dist-info/licenses}/LICENSE.md +0 -0
- {lavavu-1.8.84.dist-info → lavavu-1.9.5.dist-info}/top_level.txt +0 -0
lavavu/lavavu.py
CHANGED
@@ -14,7 +14,8 @@ See the :any:`lavavu.Viewer` class documentation for more information.
|
|
14
14
|
"""
|
15
15
|
|
16
16
|
__all__ = ['Viewer', 'Object', 'Properties', 'ColourMap', 'DrawData', 'Figure', 'Geometry', 'Image', 'Video',
|
17
|
-
'download', 'grid2d', 'grid3d', 'cubehelix', 'loadCPT', 'matplotlib_colourmap', 'printH5',
|
17
|
+
'download', 'grid2d', 'grid3d', 'cubehelix', 'loadCPT', 'matplotlib_colourmap', 'printH5',
|
18
|
+
'lerp', 'vector_magnitude', 'vector_normalise', 'vector_align',
|
18
19
|
'player', 'inject', 'hidecode', 'style', 'cellstyle', 'cellwidth',
|
19
20
|
'version', 'settings', 'is_ipython', 'is_notebook', 'getname']
|
20
21
|
|
@@ -50,6 +51,9 @@ from collections import deque
|
|
50
51
|
import time
|
51
52
|
import weakref
|
52
53
|
import asyncio
|
54
|
+
import quaternion as quat
|
55
|
+
import platform
|
56
|
+
import matplotlib
|
53
57
|
|
54
58
|
if sys.version_info[0] < 3:
|
55
59
|
print("Python 3 required. LavaVu no longer supports Python 2.7.")
|
@@ -73,8 +77,38 @@ try:
|
|
73
77
|
except (ImportError) as e:
|
74
78
|
moderngl_window = None
|
75
79
|
|
80
|
+
try:
|
81
|
+
import av
|
82
|
+
except (ImportError) as e:
|
83
|
+
av = None
|
84
|
+
|
76
85
|
#import swig module
|
77
|
-
|
86
|
+
context = os.environ.get("LV_CONTEXT", "").strip()
|
87
|
+
if platform.system() == 'Linux':
|
88
|
+
#Default context requires DISPLAY set for X11
|
89
|
+
display = os.environ.get("DISPLAY", "").strip()
|
90
|
+
if len(context) == 0: context = 'default'
|
91
|
+
if context != 'default' or len(display) == 0:
|
92
|
+
if context != 'osmesa':
|
93
|
+
try:
|
94
|
+
#Try EGL via moderngl, will use headless GPU if available
|
95
|
+
testctx = moderngl.create_context(standalone=True, require=330, backend='egl')
|
96
|
+
context = 'moderngl'
|
97
|
+
except Exception as e:
|
98
|
+
context = 'osmesa'
|
99
|
+
|
100
|
+
if context == 'osmesa':
|
101
|
+
#OSMesa fallback, CPU only, multicore
|
102
|
+
from osmesa import LavaVuPython
|
103
|
+
|
104
|
+
#Default module if none already loaded
|
105
|
+
try:
|
106
|
+
LavaVuPython
|
107
|
+
except:
|
108
|
+
import LavaVuPython
|
109
|
+
|
110
|
+
os.environ['LV_CONTEXT'] = context
|
111
|
+
|
78
112
|
version = LavaVuPython.version
|
79
113
|
server_ports = []
|
80
114
|
|
@@ -577,12 +611,12 @@ class Object(dict):
|
|
577
611
|
return key in self.dict
|
578
612
|
|
579
613
|
def __repr__(self):
|
580
|
-
return
|
614
|
+
return self.__str__()
|
581
615
|
|
582
616
|
def __str__(self):
|
583
617
|
#Default string representation
|
584
618
|
self.parent._get() #Ensure in sync
|
585
|
-
return '{\n' + str('\n'.join([' %s=%s' % (k,json.dumps(v)) for k,v in self.dict.items()])) + '\n}
|
619
|
+
return '{\n' + str('\n'.join([' %s=%s' % (k,json.dumps(v)) for k,v in self.dict.items()])) + '\n}'
|
586
620
|
|
587
621
|
#Interface for setting filters
|
588
622
|
def include(self, *args, **kwargs):
|
@@ -1111,7 +1145,12 @@ class Object(dict):
|
|
1111
1145
|
#Re-arrange to array of [r,g,b,a] values
|
1112
1146
|
data = numpy.dstack((data[0],data[1],data[2]))
|
1113
1147
|
|
1114
|
-
|
1148
|
+
#Use the shape as dims for texture image data
|
1149
|
+
if len(data.shape) > 1:
|
1150
|
+
width, height = data.shape[1], data.shape[0]
|
1151
|
+
depth = 4 #4 channel RGBA
|
1152
|
+
|
1153
|
+
self._loadScalar(data, LavaVuPython.lucRGBAData, width, height, depth)
|
1115
1154
|
|
1116
1155
|
|
1117
1156
|
def luminance(self, data):
|
@@ -1133,7 +1172,7 @@ class Object(dict):
|
|
1133
1172
|
|
1134
1173
|
self._loadScalar(data, LavaVuPython.lucLuminanceData, width, height, depth)
|
1135
1174
|
|
1136
|
-
def texture(self, data=None, flip=
|
1175
|
+
def texture(self, data=None, flip=None, filter=2, bgr=False, label=""):
|
1137
1176
|
"""
|
1138
1177
|
Load or clear texture data for object
|
1139
1178
|
|
@@ -1153,7 +1192,7 @@ class Object(dict):
|
|
1153
1192
|
If not provided, will use the default/primary texture for the object
|
1154
1193
|
flip : boolean
|
1155
1194
|
flip the texture vertically after loading
|
1156
|
-
(default is
|
1195
|
+
(default is to load value from "fliptexture" property)
|
1157
1196
|
filter : int
|
1158
1197
|
type of filtering, 0=None/nearest, 1=linear, 2=mipmap linear
|
1159
1198
|
bgr : boolean
|
@@ -1163,6 +1202,11 @@ class Object(dict):
|
|
1163
1202
|
#Clear texture
|
1164
1203
|
self.parent.app.clearTexture(self.ref)
|
1165
1204
|
return
|
1205
|
+
if flip is None:
|
1206
|
+
if "fliptexture" in self:
|
1207
|
+
flip = self["fliptexture"]
|
1208
|
+
else:
|
1209
|
+
flip = self.parent["fliptexture"]
|
1166
1210
|
if isinstance(data, str):
|
1167
1211
|
self.parent.app.setTexture(self.ref, data, flip, filter, bgr, label)
|
1168
1212
|
return
|
@@ -1233,6 +1277,8 @@ class Object(dict):
|
|
1233
1277
|
self.ref.colourMap = data.ref
|
1234
1278
|
data = None
|
1235
1279
|
else:
|
1280
|
+
if isinstance(data, matplotlib.colors.Colormap):
|
1281
|
+
data = matplotlib_cmap(data)
|
1236
1282
|
if data is None:
|
1237
1283
|
self.parent._get() #Ensure in sync
|
1238
1284
|
cmid = self["colourmap"]
|
@@ -2052,13 +2098,15 @@ class _LavaVuWrapper(LavaVuPython.LavaVu):
|
|
2052
2098
|
#Shared context
|
2053
2099
|
_ctx = None
|
2054
2100
|
|
2055
|
-
def __init__(self, threaded, runargs, resolution=None, binpath=None, context=
|
2101
|
+
def __init__(self, threaded, runargs, resolution=None, binpath=None, context=None):
|
2056
2102
|
self.args = runargs
|
2057
2103
|
self._closing = False
|
2058
2104
|
self.resolution = resolution
|
2059
2105
|
self._loop = None
|
2060
2106
|
|
2061
2107
|
#OpenGL context creation options
|
2108
|
+
if context is None:
|
2109
|
+
context = os.environ.get("LV_CONTEXT", "default")
|
2062
2110
|
havecontext = False
|
2063
2111
|
self.use_moderngl = False
|
2064
2112
|
self.use_moderngl_window = False
|
@@ -2319,11 +2367,10 @@ class _LavaVuWrapper(LavaVuPython.LavaVu):
|
|
2319
2367
|
if _LavaVuWrapper._ctx:
|
2320
2368
|
self.ctx = _LavaVuWrapper._ctx
|
2321
2369
|
else:
|
2322
|
-
import platform
|
2323
2370
|
if platform.system() == 'Linux':
|
2324
2371
|
self.ctx = moderngl.create_context(standalone=True, require=330, backend='egl')
|
2325
2372
|
else:
|
2326
|
-
self.ctx = moderngl.
|
2373
|
+
self.ctx = moderngl.create_context(standalone=True, require=330)
|
2327
2374
|
#print(self.ctx.info)
|
2328
2375
|
_LavaVuWrapper._ctx = self.ctx
|
2329
2376
|
|
@@ -2700,7 +2747,7 @@ class Viewer(dict):
|
|
2700
2747
|
|
2701
2748
|
"""
|
2702
2749
|
|
2703
|
-
def __init__(self, *args, resolution=None, binpath=None, context=
|
2750
|
+
def __init__(self, *args, resolution=None, binpath=None, context=None, port=8080, threads=False, **kwargs):
|
2704
2751
|
"""
|
2705
2752
|
Create and init viewer instance
|
2706
2753
|
|
@@ -2716,6 +2763,7 @@ class Viewer(dict):
|
|
2716
2763
|
context : str
|
2717
2764
|
OpenGL context type, *"default"* will create a context and window based on available configurations.
|
2718
2765
|
*"provided"* specifies a user provided context, set this if you have already created and activated the context.
|
2766
|
+
*"osmesa"* if built with the osmesa module, will use the self-contained software renderer to render without a window or GPU, needs to be set in LV_CONTEXT to work
|
2719
2767
|
*"moderngl"* creates a context in python using the moderngl module (experimental).
|
2720
2768
|
*"moderngl."* creates a context and a window in python using the moderngl module (experimental) specify class after separator, eg: moderngl.headless, moderngl.pyglet, moderngl.pyqt5.
|
2721
2769
|
port : int
|
@@ -2735,6 +2783,10 @@ class Viewer(dict):
|
|
2735
2783
|
self._thread = None
|
2736
2784
|
self._collections = {}
|
2737
2785
|
self.validate = True #Property validation flag
|
2786
|
+
self.recording = None
|
2787
|
+
self.context = context
|
2788
|
+
if self.context is None:
|
2789
|
+
self.context = os.environ.get('LV_CONTEXT', 'default')
|
2738
2790
|
|
2739
2791
|
#Exit handler to clean up threads
|
2740
2792
|
#(__del__ does not always seem to get called on termination)
|
@@ -2775,7 +2827,7 @@ class Viewer(dict):
|
|
2775
2827
|
if not binpath:
|
2776
2828
|
binpath = os.path.abspath(os.path.dirname(__file__))
|
2777
2829
|
try:
|
2778
|
-
self.app = _LavaVuWrapper(threads, self.args(*args, **kwargs), resolution, binpath, context)
|
2830
|
+
self.app = _LavaVuWrapper(threads, self.args(*args, **kwargs), resolution, binpath, self.context)
|
2779
2831
|
except (RuntimeError) as e:
|
2780
2832
|
print("LavaVu Init error: " + str(e))
|
2781
2833
|
pass
|
@@ -3212,10 +3264,12 @@ class Viewer(dict):
|
|
3212
3264
|
self.validate = self.state["properties"]["validate"]
|
3213
3265
|
self._objects._sync()
|
3214
3266
|
|
3215
|
-
def _set(self):
|
3267
|
+
def _set(self, reload=None):
|
3216
3268
|
#Export state to lavavu
|
3217
3269
|
#(include current object list state)
|
3218
3270
|
#self.state["objects"] = [obj.dict for obj in self._objects.list]
|
3271
|
+
if reload is not None:
|
3272
|
+
self.state["reload"] = reload
|
3219
3273
|
self.app.setState(json.dumps(self.state))
|
3220
3274
|
|
3221
3275
|
def commands(self, cmds, queue=False):
|
@@ -3695,7 +3749,7 @@ class Viewer(dict):
|
|
3695
3749
|
else:
|
3696
3750
|
return c.tolist()
|
3697
3751
|
|
3698
|
-
def texture(self, label, data=None, flip=
|
3752
|
+
def texture(self, label, data=None, flip=None, filter=2, bgr=False):
|
3699
3753
|
"""
|
3700
3754
|
Load or clear global/shared texture data
|
3701
3755
|
|
@@ -3714,7 +3768,7 @@ class Viewer(dict):
|
|
3714
3768
|
Pass a string to load a texture from given filename
|
3715
3769
|
flip : boolean
|
3716
3770
|
flip the texture vertically after loading
|
3717
|
-
(default is
|
3771
|
+
(default is to load value from "fliptexture" property)
|
3718
3772
|
filter : int
|
3719
3773
|
type of filtering, 0=None/nearest, 1=linear, 2=mipmap linear
|
3720
3774
|
bgr : boolean
|
@@ -3724,6 +3778,8 @@ class Viewer(dict):
|
|
3724
3778
|
#Clear texture
|
3725
3779
|
self.app.clearTexture(None, label)
|
3726
3780
|
return
|
3781
|
+
if flip is None:
|
3782
|
+
flip = self["fliptexture"]
|
3727
3783
|
if isinstance(data, str):
|
3728
3784
|
self.app.setTexture(None, data, flip, filter, bgr, label)
|
3729
3785
|
return
|
@@ -3821,6 +3877,9 @@ class Viewer(dict):
|
|
3821
3877
|
Render a new frame, explicit display update
|
3822
3878
|
"""
|
3823
3879
|
self.app.render()
|
3880
|
+
#Video recording?
|
3881
|
+
if self.recording:
|
3882
|
+
self.recording.frame()
|
3824
3883
|
|
3825
3884
|
def init(self):
|
3826
3885
|
"""
|
@@ -4128,7 +4187,7 @@ class Viewer(dict):
|
|
4128
4187
|
from IPython.display import display,HTML,Javascript
|
4129
4188
|
display(Javascript(js + code))
|
4130
4189
|
|
4131
|
-
def video(self, filename="", fps=30, quality=
|
4190
|
+
def video(self, filename="", resolution=(0,0), fps=30, quality=0, encoder="h264", player=None, options={}, **kwargs):
|
4132
4191
|
"""
|
4133
4192
|
Record and show the generated video inline within an ipython notebook.
|
4134
4193
|
|
@@ -4145,25 +4204,36 @@ class Viewer(dict):
|
|
4145
4204
|
----------
|
4146
4205
|
filename : str
|
4147
4206
|
Name of the file to save, if not provided a default will be used
|
4207
|
+
resolution : list or tuple
|
4208
|
+
Video resolution in pixels [x,y]
|
4148
4209
|
fps : int
|
4149
4210
|
Frames to output per second of video
|
4150
4211
|
quality : int
|
4151
|
-
Encoding quality, 1=low
|
4212
|
+
Encoding quality, 1=low, 2=medium, 3=high, higher quality reduces
|
4152
4213
|
encoding artifacts at cost of larger file size
|
4153
|
-
|
4154
|
-
|
4214
|
+
If omitted will use default settings, can fine tune settings in kwargs
|
4215
|
+
encoder : str
|
4216
|
+
Name of encoder to use, eg: "h264" (default), "mpeg"
|
4217
|
+
player : dict
|
4218
|
+
Args to pass to the player when the video is finished, eg:
|
4219
|
+
{"width" : 800, "height", 400, "params": "controls autoplay"}
|
4220
|
+
options : dict
|
4221
|
+
Args to pass to the encoder, eg: see
|
4222
|
+
https://trac.ffmpeg.org/wiki/Encode/H.264
|
4155
4223
|
**kwargs :
|
4156
|
-
Any additional keyword args will be passed to
|
4224
|
+
Any additional keyword args will also be passed as options to the encoder
|
4157
4225
|
|
4158
4226
|
Returns
|
4159
4227
|
-------
|
4160
4228
|
recorder : Video(object)
|
4161
4229
|
Context manager object that controls the video recording
|
4162
4230
|
"""
|
4163
|
-
return Video(self, filename, resolution, fps, quality, **kwargs)
|
4231
|
+
return Video(self, filename, resolution, fps, quality, encoder, player, options, **kwargs)
|
4164
4232
|
|
4165
|
-
def video_steps(self, filename="", start=0, end=0, fps=
|
4233
|
+
def video_steps(self, filename="", start=0, end=0, resolution=(0,0), fps=30, quality=2, encoder="h264", player=None, options={}, **kwargs):
|
4166
4234
|
"""
|
4235
|
+
TODO: Fix to use pyAV
|
4236
|
+
|
4167
4237
|
Record a video of the model by looping through all time steps
|
4168
4238
|
|
4169
4239
|
Shows the generated video inline within an ipython notebook.
|
@@ -4181,20 +4251,26 @@ class Viewer(dict):
|
|
4181
4251
|
First timestep to record, if not specified will use first available
|
4182
4252
|
end : int
|
4183
4253
|
Last timestep to record, if not specified will use last available
|
4254
|
+
resolution : list or tuple
|
4255
|
+
Video resolution in pixels [x,y]
|
4184
4256
|
fps : int
|
4185
4257
|
Frames to output per second of video
|
4186
4258
|
quality : int
|
4187
|
-
Encoding quality, 1=low
|
4259
|
+
Encoding quality, 1=low, 2=medium(default), 3=high, higher quality reduces
|
4188
4260
|
encoding artifacts at cost of larger file size
|
4189
|
-
resolution : list or tuple
|
4190
|
-
Video resolution in pixels [x,y]
|
4191
4261
|
**kwargs :
|
4192
|
-
Any additional keyword args will be passed to
|
4262
|
+
Any additional keyword args will also be passed as options to the encoder
|
4193
4263
|
"""
|
4194
4264
|
|
4195
4265
|
try:
|
4196
|
-
|
4197
|
-
|
4266
|
+
steps = self.steps
|
4267
|
+
if end == 0: end = len(steps)
|
4268
|
+
steps = steps[start:end]
|
4269
|
+
from tqdm.notebook import tqdm
|
4270
|
+
with Video(self, filename, resolution, fps, quality, encoder, player, options, **kwargs):
|
4271
|
+
for s in tqdm(steps, desc='Rendering loop'):
|
4272
|
+
self.timestep(s)
|
4273
|
+
self.render()
|
4198
4274
|
except (Exception) as e:
|
4199
4275
|
print("Video output error: " + str(e))
|
4200
4276
|
pass
|
@@ -4378,7 +4454,7 @@ class Viewer(dict):
|
|
4378
4454
|
#Issue redisplay to active viewer
|
4379
4455
|
self.control.redisplay()
|
4380
4456
|
|
4381
|
-
def camera(self, data=None):
|
4457
|
+
def camera(self, data=None, quiet=False):
|
4382
4458
|
"""
|
4383
4459
|
Get/set the current camera viewpoint
|
4384
4460
|
|
@@ -4389,6 +4465,8 @@ class Viewer(dict):
|
|
4389
4465
|
----------
|
4390
4466
|
data : dict
|
4391
4467
|
Camera view to apply if any
|
4468
|
+
quiet : bool
|
4469
|
+
Skip print and calling "camera" script command which produces noisy terminal output
|
4392
4470
|
|
4393
4471
|
Returns
|
4394
4472
|
-------
|
@@ -4399,7 +4477,8 @@ class Viewer(dict):
|
|
4399
4477
|
me = getname(self)
|
4400
4478
|
if not me: me = "lv"
|
4401
4479
|
#Also print in terminal for debugging
|
4402
|
-
|
4480
|
+
if not quiet and not data:
|
4481
|
+
self.commands("camera")
|
4403
4482
|
#Get: export from first view
|
4404
4483
|
vdat = {}
|
4405
4484
|
if len(self.state["views"]) and self.state["views"][0]:
|
@@ -4407,27 +4486,110 @@ class Viewer(dict):
|
|
4407
4486
|
for key in ["translate", "rotate", "xyzrotate", "fov", "focus"]:
|
4408
4487
|
if key in src:
|
4409
4488
|
dst[key] = copy.copy(src[key])
|
4410
|
-
#Round down arrays to max
|
4489
|
+
#Round down arrays to max 6 decimal places
|
4411
4490
|
try:
|
4412
4491
|
for r in range(len(dst[key])):
|
4413
|
-
dst[key][r] = round(dst[key][r],
|
4492
|
+
dst[key][r] = round(dst[key][r], 6)
|
4414
4493
|
except:
|
4415
4494
|
#Not a list/array
|
4416
4495
|
pass
|
4417
4496
|
|
4418
4497
|
copyview(vdat, self.state["views"][0])
|
4419
4498
|
|
4420
|
-
|
4421
|
-
|
4499
|
+
if not quiet and not data:
|
4500
|
+
print(me + ".translation(" + str(vdat["translate"])[1:-1] + ")")
|
4501
|
+
print(me + ".rotation(" + str(vdat["xyzrotate"])[1:-1] + ")")
|
4422
4502
|
|
4423
4503
|
#Set
|
4424
4504
|
if data is not None:
|
4425
4505
|
copyview(self.state["views"][0], data)
|
4426
|
-
self._set()
|
4506
|
+
self._set(reload=False)
|
4427
4507
|
|
4428
4508
|
#Return
|
4429
4509
|
return vdat
|
4430
4510
|
|
4511
|
+
def lookat(self, pos, at=None, up=None):
|
4512
|
+
"""
|
4513
|
+
Set the camera with a position coord and lookat coord
|
4514
|
+
|
4515
|
+
Parameters
|
4516
|
+
----------
|
4517
|
+
pos : list/numpy.ndarray
|
4518
|
+
Camera position in world coords
|
4519
|
+
lookat : list/numpy.ndarray
|
4520
|
+
Look at position in world coords, defaults to model origin
|
4521
|
+
up : list/numpy.ndarray
|
4522
|
+
Up vector, defaults to Y axis [0,1,0]
|
4523
|
+
"""
|
4524
|
+
|
4525
|
+
# Use the origin from viewer if no target provided
|
4526
|
+
if at is None:
|
4527
|
+
at = self["focus"]
|
4528
|
+
else:
|
4529
|
+
self.focus(*at)
|
4530
|
+
|
4531
|
+
# Default to Y-axis up vector
|
4532
|
+
if up is None:
|
4533
|
+
up = numpy.array([0, 1, 0])
|
4534
|
+
|
4535
|
+
# Calculate the rotation matrix
|
4536
|
+
heading = numpy.array(pos) - numpy.array(at)
|
4537
|
+
zd = vector_normalise(heading)
|
4538
|
+
xd = vector_normalise(numpy.cross(up, zd))
|
4539
|
+
yd = vector_normalise(numpy.cross(zd, xd))
|
4540
|
+
q = quat.from_rotation_matrix(numpy.array([xd, yd, zd]))
|
4541
|
+
q = q.normalized()
|
4542
|
+
|
4543
|
+
# Apply the rotation
|
4544
|
+
self.rotation(q.x, q.y, q.z, q.w)
|
4545
|
+
|
4546
|
+
# Translate back by heading vector length in Z
|
4547
|
+
# (model origin in lavavu takes care of lookat offset)
|
4548
|
+
tr = [0, 0, -vector_magnitude(numpy.array(pos) - numpy.array(at))]
|
4549
|
+
|
4550
|
+
# Apply translation
|
4551
|
+
self.translation(tr)
|
4552
|
+
|
4553
|
+
def camlerpto(self, pos, L):
|
4554
|
+
# Lerp using current camera orientation as start point
|
4555
|
+
pos0 = self.camera(quiet=True)
|
4556
|
+
return self.camlerp(pos0, pos)
|
4557
|
+
|
4558
|
+
def camlerp(self, pos0, pos1, L):
|
4559
|
+
"""
|
4560
|
+
Linearly Interpolate between two camera positions/orientations and
|
4561
|
+
set the camera to the resulting position/orientation
|
4562
|
+
"""
|
4563
|
+
final = {}
|
4564
|
+
for key in ["translate", "rotate", "focus"]:
|
4565
|
+
val0 = numpy.array(pos0[key])
|
4566
|
+
val1 = numpy.array(pos1[key])
|
4567
|
+
res = val0 + (val1 - val0) * L
|
4568
|
+
if len(res) > 3:
|
4569
|
+
# Normalise quaternion
|
4570
|
+
res = res / numpy.linalg.norm(res)
|
4571
|
+
final[key] = res.tolist()
|
4572
|
+
|
4573
|
+
self.camera(final, quiet=True)
|
4574
|
+
|
4575
|
+
def flyto(self, pos, steps, stop=False, callback=None):
|
4576
|
+
# Fly using current camera orientation as start point
|
4577
|
+
pos0 = self.camera(quiet=True)
|
4578
|
+
return self.fly(pos0, pos, steps, stop, callback)
|
4579
|
+
|
4580
|
+
def fly(self, pos0, pos1, steps, stop=False, callback=None):
|
4581
|
+
self.camera(pos0, quiet=True)
|
4582
|
+
self.render()
|
4583
|
+
|
4584
|
+
for i in range(steps):
|
4585
|
+
if stop and i > stop:
|
4586
|
+
break
|
4587
|
+
L = i / (steps - 1)
|
4588
|
+
self.camlerp(pos0, pos1, L)
|
4589
|
+
if callback is not None:
|
4590
|
+
callback(self, i, steps)
|
4591
|
+
self.render()
|
4592
|
+
|
4431
4593
|
def getview(self):
|
4432
4594
|
"""
|
4433
4595
|
Get current view settings
|
@@ -5256,54 +5418,42 @@ def player(filename, params="controls autoplay loop", **kwargs):
|
|
5256
5418
|
import uuid
|
5257
5419
|
vid = 'video_' + str(uuid.uuid4())[:8]
|
5258
5420
|
|
5259
|
-
'''
|
5260
|
-
def get_fn(filename):
|
5261
|
-
import os
|
5262
|
-
#This is unreliable, path incorrect on NCI
|
5263
|
-
nbpath = os.getenv('JPY_SESSION_NAME')
|
5264
|
-
if nbpath is not None:
|
5265
|
-
import os.path
|
5266
|
-
relpath = os.path.relpath(os.getcwd(), start=os.path.dirname(nbpath))
|
5267
|
-
return relpath + '/' + filename
|
5268
|
-
return filename
|
5269
|
-
''';
|
5270
|
-
|
5271
|
-
import uuid
|
5272
|
-
uid = uuid.uuid1()
|
5273
|
-
|
5274
|
-
#Embed player
|
5275
|
-
display(Video(url=os.path.relpath(filename), html_attributes=f"id='{vid}' " + params, **kwargs))
|
5276
|
-
|
5277
|
-
#Add download link
|
5278
|
-
display(HTML(f'<a id="link_{vid}" href="{filename}" download>Download Video</a>'))
|
5279
|
-
|
5280
5421
|
# Fallback - replace url on gadi and similar jupyterhub installs with
|
5281
5422
|
# fixed working directory that doesn't match notebook dir
|
5282
5423
|
# check the video tag url and remove subpath on 404 error
|
5283
5424
|
display(Javascript(f"""
|
5284
|
-
|
5285
|
-
|
5286
|
-
fetch(url, {{method: 'HEAD'}}).then(response=>{{
|
5287
|
-
if(response.status == 404) {{
|
5425
|
+
function video_error(el) {{
|
5426
|
+
let url = el.src;
|
5288
5427
|
console.log("Bad video url: " + url);
|
5289
5428
|
let toppath = "/files/home/"
|
5290
5429
|
let baseurl = url.substring(0, url.indexOf(toppath)+toppath.length);
|
5291
5430
|
let endurl = url.substring(url.indexOf("{filename}"));
|
5292
5431
|
let fixed = baseurl + endurl;
|
5293
|
-
|
5294
|
-
|
5295
|
-
|
5296
|
-
|
5297
|
-
|
5298
|
-
|
5432
|
+
if (url != fixed) {{
|
5433
|
+
console.log("Replaced video url: " + fixed);
|
5434
|
+
el.src = fixed;
|
5435
|
+
//Also fix download link
|
5436
|
+
document.getElementById('link_{vid}').href = fixed;
|
5437
|
+
}} else {{
|
5438
|
+
console.log("Not replacing video url, no change: " + fixed);
|
5439
|
+
}}
|
5440
|
+
}}
|
5299
5441
|
"""))
|
5300
|
-
|
5442
|
+
|
5443
|
+
#Embed player
|
5444
|
+
filename = os.path.relpath(filename)
|
5445
|
+
display(Video(url=filename, html_attributes=f'id="{vid}" onerror="video_error(this)"' + params, **kwargs))
|
5446
|
+
|
5447
|
+
#Add download link
|
5448
|
+
display(HTML(f'<a id="link_{vid}" href="{filename}" download>Download Video</a>'))
|
5301
5449
|
|
5302
5450
|
#Class for managing video animation recording
|
5303
5451
|
class Video(object):
|
5304
5452
|
"""
|
5305
5453
|
The Video class provides an interface to record animations
|
5306
5454
|
|
5455
|
+
This now uses pyAV avoiding the need to build LavaVu with ffmpeg support
|
5456
|
+
|
5307
5457
|
Example
|
5308
5458
|
-------
|
5309
5459
|
|
@@ -5315,7 +5465,7 @@ class Video(object):
|
|
5315
5465
|
... lv.rotate('y', 10) # doctest: +SKIP
|
5316
5466
|
... lv.render() # doctest: +SKIP
|
5317
5467
|
"""
|
5318
|
-
def __init__(self, viewer
|
5468
|
+
def __init__(self, viewer, filename="output.mp4", resolution=(0,0), framerate=30, quality=2, encoder="h264", player=None, options={}, **kwargs):
|
5319
5469
|
"""
|
5320
5470
|
Record and show the generated video inline within an ipython notebook.
|
5321
5471
|
|
@@ -5335,79 +5485,149 @@ class Video(object):
|
|
5335
5485
|
fps : int
|
5336
5486
|
Frames to output per second of video
|
5337
5487
|
quality : int
|
5338
|
-
Encoding quality, 1=low
|
5488
|
+
Encoding quality, 1=low, 2=medium(default), 3=high, higher quality reduces
|
5339
5489
|
encoding artifacts at cost of larger file size
|
5490
|
+
If omitted will use default settings, can fine tune settings in kwargs
|
5340
5491
|
resolution : list or tuple
|
5341
5492
|
Video resolution in pixels [x,y]
|
5493
|
+
encoder : str
|
5494
|
+
Name of encoder to use, eg: "h264" (default), "mpeg"
|
5495
|
+
player : dict
|
5496
|
+
Args to pass to the player when the video is finished, eg:
|
5497
|
+
{"width" : 800, "height", 400, "params": "controls autoplay"}
|
5498
|
+
options : dict
|
5499
|
+
Args to pass to the encoder, eg: see
|
5500
|
+
https://trac.ffmpeg.org/wiki/Encode/H.264
|
5342
5501
|
**kwargs :
|
5343
|
-
Any additional keyword args will be passed to
|
5502
|
+
Any additional keyword args will also be passed as options to the encoder
|
5344
5503
|
"""
|
5345
|
-
|
5504
|
+
if av is None:
|
5505
|
+
raise(ImportError("Video output not supported without pyAV - pip install av"))
|
5506
|
+
return
|
5507
|
+
self.resolution = list(resolution)
|
5508
|
+
if self.resolution[0] == 0 or self.resolution[1] == 0:
|
5509
|
+
self.resolution = (viewer.app.viewer.width, viewer.app.viewer.height)
|
5510
|
+
#Ensure resolution values are even or encoder fails
|
5511
|
+
self.resolution = (2 * int(self.resolution[0]//2), 2 * int(self.resolution[1] // 2))
|
5346
5512
|
self.framerate = framerate
|
5347
5513
|
self.quality = quality
|
5348
5514
|
self.viewer = viewer
|
5349
5515
|
self.filename = filename
|
5350
|
-
self.
|
5351
|
-
if
|
5352
|
-
|
5353
|
-
|
5354
|
-
|
5516
|
+
self.player = player
|
5517
|
+
if self.player is None:
|
5518
|
+
#Default player is half output resolution, unless < 900 then full
|
5519
|
+
if "width" in kwargs or "height" in kwargs or "params" in kwargs:
|
5520
|
+
#Back compatibility with old args
|
5521
|
+
self.player = {}
|
5522
|
+
self.player.update(kwargs)
|
5523
|
+
kwargs = {}
|
5524
|
+
elif self.resolution[0] > 900:
|
5525
|
+
#Larger output res - default to half size player
|
5526
|
+
self.player = {"width": self.resolution[0]//2, "height": self.resolution[1]//2}
|
5527
|
+
else:
|
5528
|
+
self.player = {}
|
5529
|
+
self.encoder = encoder
|
5530
|
+
self.options = options
|
5531
|
+
#Also include extra args
|
5532
|
+
options.update(kwargs)
|
5533
|
+
self.container = None
|
5355
5534
|
|
5356
5535
|
def start(self):
|
5357
5536
|
"""
|
5358
5537
|
Start recording, all rendered frames will be added to the video
|
5359
5538
|
"""
|
5360
|
-
|
5361
|
-
|
5362
|
-
|
5363
|
-
|
5364
|
-
|
5365
|
-
|
5366
|
-
|
5367
|
-
|
5368
|
-
|
5369
|
-
|
5539
|
+
#https://trac.ffmpeg.org/wiki/Encode/H.264
|
5540
|
+
# The range of the CRF scale is 0–51, where 0 is lossless (for 8 bit only, for 10 bit use -qp 0),
|
5541
|
+
# 23 is the default, and 51 is worst quality possible
|
5542
|
+
#Compression level, lower = high quality
|
5543
|
+
#See also: https://github.com/PyAV-Org/PyAV/blob/main/tests/test_encode.py
|
5544
|
+
options = {}
|
5545
|
+
if self.encoder == 'h264' or self.encoder == 'libx265':
|
5546
|
+
#Only have default options for h264/265 for now
|
5547
|
+
if self.quality == 1:
|
5548
|
+
options['qmin'] = '30' #'20' #'8'
|
5549
|
+
options['qmax'] = '35' #'41'
|
5550
|
+
options['crf'] = '30' #'40'
|
5551
|
+
elif self.quality == 2:
|
5552
|
+
options['qmin'] = '25' #'2'
|
5553
|
+
options['qmax'] = '30' #'31'
|
5554
|
+
options['crf'] = '20' #'23'
|
5555
|
+
elif self.quality == 3:
|
5556
|
+
options['qmin'] = '20' #'1'
|
5557
|
+
options['qmax'] = '25' #'4'
|
5558
|
+
options['crf'] = '10' #'10'
|
5559
|
+
|
5560
|
+
#Default preset, tune for h264
|
5561
|
+
options["preset"] = 'veryfast' #'veryfast' 'medium' 'slow'
|
5562
|
+
options["tune"] = 'animation' #'film'
|
5563
|
+
|
5564
|
+
#Settings from our original c++ encoder
|
5565
|
+
#self.options['i_quant_factor'] = '0.71'
|
5566
|
+
#self.options['qcompress'] = '0.6'
|
5567
|
+
#self.options['max_qdiff'] = '4'
|
5568
|
+
#self.options['refs'] = '3'
|
5569
|
+
|
5570
|
+
#Merge user options, allowing override of above settings
|
5571
|
+
options.update(self.options)
|
5572
|
+
|
5573
|
+
#print(options)
|
5574
|
+
self.container = av.open(self.filename, mode="w")
|
5575
|
+
self.stream = self.container.add_stream(self.encoder, rate=self.framerate, options=options)
|
5576
|
+
self.stream.width = self.resolution[0]
|
5577
|
+
self.stream.height = self.resolution[1]
|
5578
|
+
self.stream.pix_fmt = "yuv420p"
|
5579
|
+
self.viewer.recording = self
|
5580
|
+
|
5581
|
+
stream = self.stream
|
5582
|
+
#print(stream)
|
5583
|
+
#print(stream.codec)
|
5584
|
+
#print(stream.codec_context)
|
5585
|
+
#print(stream.profiles)
|
5586
|
+
#print(stream.profile)
|
5587
|
+
#print(stream.options)
|
5588
|
+
#Need to set profile here or it isn't applied
|
5589
|
+
#(Default to main)
|
5590
|
+
stream.profile = 'Main' #Baseline / High
|
5591
|
+
cc = stream.codec_context
|
5592
|
+
#print(cc.options)
|
5593
|
+
#print(cc.profile)
|
5594
|
+
|
5595
|
+
def frame(self):
|
5596
|
+
"""
|
5597
|
+
Write a frame, called when viewer.render() is called
|
5598
|
+
while a recording is in progress
|
5599
|
+
"""
|
5600
|
+
img = self.viewer.rawimage(resolution=self.resolution, channels=3)
|
5601
|
+
frame = av.VideoFrame.from_ndarray(img.data, format="rgb24")
|
5602
|
+
for packet in self.stream.encode(frame):
|
5603
|
+
self.container.mux(packet)
|
5370
5604
|
|
5371
5605
|
def pause(self):
|
5372
5606
|
"""
|
5373
5607
|
Pause/resume recording, no rendered frames will be added to the video while paused
|
5374
5608
|
"""
|
5375
|
-
if self.
|
5376
|
-
self.
|
5609
|
+
if self.viewer.recording:
|
5610
|
+
self.viewer.recording = None
|
5377
5611
|
else:
|
5378
|
-
self.viewer.
|
5612
|
+
self.viewer.recording = self
|
5379
5613
|
|
5380
5614
|
def stop(self):
|
5381
5615
|
"""
|
5382
5616
|
Stop recording, final frames will be written and file closed, ready to play.
|
5383
5617
|
No further frames will be added to the video
|
5384
5618
|
"""
|
5385
|
-
|
5386
|
-
|
5387
|
-
self.
|
5388
|
-
|
5389
|
-
|
5390
|
-
|
5391
|
-
|
5392
|
-
|
5393
|
-
log = ""
|
5394
|
-
ffmpeg_exe = 'ffmpeg'
|
5395
|
-
try:
|
5396
|
-
outfn = '{0}.mp4'.format(self.filename)
|
5397
|
-
cmd = ffmpeg_exe + ' -r {1} -i "{0}/frame_%05d.jpg" -c:v libx264 -vf fps={1} -movflags +faststart -pix_fmt yuv420p -y "{2}"'.format(self.filename, self.framerate, outfn)
|
5398
|
-
log += "No built in video encoding, attempting to build movie from frames with ffmpeg:\n"
|
5399
|
-
log += cmd
|
5400
|
-
|
5401
|
-
import subprocess
|
5402
|
-
#os.system(cmd)
|
5403
|
-
subprocess.check_call(cmd, shell=True)
|
5404
|
-
self.filename = outfn
|
5405
|
-
except (Exception) as e:
|
5406
|
-
print("Video encoding failed: ", str(e), "\nlog:\n", log)
|
5619
|
+
# Flush stream
|
5620
|
+
for packet in self.stream.encode():
|
5621
|
+
self.container.mux(packet)
|
5622
|
+
# Close the file
|
5623
|
+
self.container.close()
|
5624
|
+
self.container = None
|
5625
|
+
self.stream = None
|
5626
|
+
self.viewer.recording = None
|
5407
5627
|
|
5408
5628
|
def write(self, image):
|
5409
5629
|
"""
|
5410
|
-
Add a frame to the video
|
5630
|
+
Add a frame to the video
|
5411
5631
|
|
5412
5632
|
Parameters
|
5413
5633
|
----------
|
@@ -5415,7 +5635,7 @@ class Video(object):
|
|
5415
5635
|
Pass a list or numpy uint8 array of rgba values or an Image object
|
5416
5636
|
values are loaded as 8 bit unsigned integer values
|
5417
5637
|
"""
|
5418
|
-
if self.
|
5638
|
+
if self.container:
|
5419
5639
|
if isinstance(image, Image):
|
5420
5640
|
image = image.data
|
5421
5641
|
else:
|
@@ -5423,13 +5643,17 @@ class Video(object):
|
|
5423
5643
|
if image.size == self.resolution[0] * self.resolution[1] * 4:
|
5424
5644
|
image = image.reshape(self.resolution[0], self.resolution[1], 4)
|
5425
5645
|
image = image[::,::,:3] #Remove alpha channel
|
5426
|
-
self.encoder.copyframe(image.ravel())
|
5646
|
+
#self.encoder.copyframe(image.ravel())
|
5647
|
+
|
5648
|
+
frame = av.VideoFrame.from_ndarray(image, format="rgb24")
|
5649
|
+
for packet in self.stream.encode(frame):
|
5650
|
+
self.container.mux(packet)
|
5427
5651
|
|
5428
5652
|
def play(self):
|
5429
5653
|
"""
|
5430
|
-
Show the video in an inline player if in an
|
5654
|
+
Show the video in an inline player if in an interactive notebook
|
5431
5655
|
"""
|
5432
|
-
player(self.filename, **self.
|
5656
|
+
player(self.filename, **self.player)
|
5433
5657
|
|
5434
5658
|
def __enter__(self):
|
5435
5659
|
self.start()
|
@@ -5848,6 +6072,56 @@ def lerp(first, second, mu):
|
|
5848
6072
|
final[i] += diff * mu
|
5849
6073
|
return final
|
5850
6074
|
|
6075
|
+
def vector_magnitude(vec):
|
6076
|
+
return numpy.linalg.norm(vec)
|
6077
|
+
|
6078
|
+
def vector_normalise(vec):
|
6079
|
+
norm = numpy.linalg.norm(vec)
|
6080
|
+
if norm == 0:
|
6081
|
+
vn = vec
|
6082
|
+
else:
|
6083
|
+
vn = vec / norm
|
6084
|
+
return vn
|
6085
|
+
|
6086
|
+
def vector_align(v1, v2, lvformat=True):
|
6087
|
+
"""
|
6088
|
+
Get a rotation quaterion to align vectors v1 with v2
|
6089
|
+
|
6090
|
+
Parameters
|
6091
|
+
----------
|
6092
|
+
v1 : list/numpy.ndarray
|
6093
|
+
First 3 component vector
|
6094
|
+
v2 : list/numpy.ndarray
|
6095
|
+
Second 3 component vector to align the first to
|
6096
|
+
|
6097
|
+
Returns
|
6098
|
+
-------
|
6099
|
+
list: quaternion to rotate v1 to v2 (in lavavu format)
|
6100
|
+
"""
|
6101
|
+
|
6102
|
+
# Check for parallel or opposite
|
6103
|
+
v1 = vector_normalise(numpy.array(v1))
|
6104
|
+
v2 = vector_normalise(numpy.array(v2))
|
6105
|
+
epsilon = numpy.finfo(numpy.float32).eps
|
6106
|
+
one_minus_eps = 1.0 - epsilon
|
6107
|
+
if numpy.dot(v1, v2) > one_minus_eps: # 1.0
|
6108
|
+
# No rotation
|
6109
|
+
return [0, 0, 0, 1]
|
6110
|
+
elif numpy.dot(v1, v2) < -one_minus_eps: # -1.0
|
6111
|
+
# 180 rotation about Y
|
6112
|
+
return [0, 1, 0, 1]
|
6113
|
+
xyz = numpy.cross(v1, v2)
|
6114
|
+
l1 = numpy.linalg.norm(v1)
|
6115
|
+
l2 = numpy.linalg.norm(v2)
|
6116
|
+
w = math.sqrt((l1 * l1) * (l2 * l2)) + numpy.dot(v1, v2)
|
6117
|
+
qr = quat.quaternion(w, xyz[0], xyz[1], xyz[2])
|
6118
|
+
qr = qr.normalized()
|
6119
|
+
# Return in LavaVu quaternion format
|
6120
|
+
if lvformat:
|
6121
|
+
return [qr.x, qr.y, qr.z, qr.w]
|
6122
|
+
else:
|
6123
|
+
return qr
|
6124
|
+
|
5851
6125
|
if __name__ == '__main__':
|
5852
6126
|
#Run doctests - only works on numpy 1.14+ due to changes in array printing
|
5853
6127
|
npyv = numpy.__version__.split('.')
|
@@ -5857,3 +6131,4 @@ if __name__ == '__main__':
|
|
5857
6131
|
import doctest
|
5858
6132
|
doctest.testmod()
|
5859
6133
|
|
6134
|
+
|