lavavu 1.8.83__cp312-cp312-macosx_11_0_arm64.whl → 1.9.0__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 +14 -2
- lavavu/html/webview.html +1 -1
- lavavu/lavavu.py +324 -68
- 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-1.8.83.dist-info → lavavu-1.9.0.dist-info}/METADATA +5 -4
- {lavavu-1.8.83.dist-info → lavavu-1.9.0.dist-info}/RECORD +22 -20
- {lavavu-1.8.83.dist-info → lavavu-1.9.0.dist-info}/WHEEL +1 -1
- {lavavu-1.8.83.dist-info → lavavu-1.9.0.dist-info}/LICENSE.md +0 -0
- {lavavu-1.8.83.dist-info → lavavu-1.9.0.dist-info}/entry_points.txt +0 -0
- {lavavu-1.8.83.dist-info → lavavu-1.9.0.dist-info}/top_level.txt +0 -0
Binary file
|
lavavu/control.py
CHANGED
@@ -481,8 +481,20 @@ class Window(_Container):
|
|
481
481
|
self.align = align
|
482
482
|
self.wrapper = wrapper
|
483
483
|
self.fullscreen = fullscreen
|
484
|
-
if resolution is
|
485
|
-
|
484
|
+
if resolution is None:
|
485
|
+
#Default to largest of 640x480 or output res / 2
|
486
|
+
resolution = [0,0]
|
487
|
+
resolution[0] = max(viewer.output_resolution[0]//2, 640)
|
488
|
+
resolution[1] = int(resolution[0] * viewer.output_resolution[1]/viewer.output_resolution[0])
|
489
|
+
elif isinstance(resolution, int):
|
490
|
+
#Passed interger - interpert as width
|
491
|
+
resolution = [resolution,0]
|
492
|
+
|
493
|
+
if resolution[1] == 0:
|
494
|
+
#Width only, set height based on output aspect ratio
|
495
|
+
resolution[1] = int(resolution[0] * viewer.output_resolution[1]/viewer.output_resolution[0])
|
496
|
+
|
497
|
+
viewer.output_resolution = resolution
|
486
498
|
|
487
499
|
def html(self):
|
488
500
|
#print(self.viewer["resolution"], self.viewer.output_resolution)
|
lavavu/html/webview.html
CHANGED
@@ -27,7 +27,7 @@
|
|
27
27
|
|
28
28
|
<input id="fileinput" type="file" style="visibility:hidden" onchange="useFileInput(this)" />
|
29
29
|
|
30
|
-
<script async src="https://cdn.jsdelivr.net/gh/lavavu/lavavu.github.io@1.
|
30
|
+
<script async src="https://cdn.jsdelivr.net/gh/lavavu/lavavu.github.io@1.9.0/LavaVu-amalgamated.min.js"></script>
|
31
31
|
<!--script src="dat.gui.min.js"></script>
|
32
32
|
<script src="OK-min.js"></script>
|
33
33
|
|
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,8 @@ from collections import deque
|
|
50
51
|
import time
|
51
52
|
import weakref
|
52
53
|
import asyncio
|
54
|
+
import quaternion as quat
|
55
|
+
import platform
|
53
56
|
|
54
57
|
if sys.version_info[0] < 3:
|
55
58
|
print("Python 3 required. LavaVu no longer supports Python 2.7.")
|
@@ -73,8 +76,38 @@ try:
|
|
73
76
|
except (ImportError) as e:
|
74
77
|
moderngl_window = None
|
75
78
|
|
79
|
+
try:
|
80
|
+
import av
|
81
|
+
except (ImportError) as e:
|
82
|
+
av = None
|
83
|
+
|
76
84
|
#import swig module
|
77
|
-
|
85
|
+
context = os.environ.get("LV_CONTEXT", "").strip()
|
86
|
+
if platform.system() == 'Linux':
|
87
|
+
#Default context requires DISPLAY set for X11
|
88
|
+
display = os.environ.get("DISPLAY", "").strip()
|
89
|
+
if len(context) == 0: context = 'default'
|
90
|
+
if context != 'default' or len(display) == 0:
|
91
|
+
if context != 'osmesa':
|
92
|
+
try:
|
93
|
+
#Try EGL via moderngl, will use headless GPU if available
|
94
|
+
testctx = moderngl.create_context(standalone=True, require=330, backend='egl')
|
95
|
+
context = 'moderngl'
|
96
|
+
except Exception as e:
|
97
|
+
context = 'osmesa'
|
98
|
+
|
99
|
+
if context == 'osmesa':
|
100
|
+
#OSMesa fallback, CPU only, multicore
|
101
|
+
from osmesa import LavaVuPython
|
102
|
+
|
103
|
+
#Default module if none already loaded
|
104
|
+
try:
|
105
|
+
LavaVuPython
|
106
|
+
except:
|
107
|
+
import LavaVuPython
|
108
|
+
|
109
|
+
os.environ['LV_CONTEXT'] = context
|
110
|
+
|
78
111
|
version = LavaVuPython.version
|
79
112
|
server_ports = []
|
80
113
|
|
@@ -2052,13 +2085,15 @@ class _LavaVuWrapper(LavaVuPython.LavaVu):
|
|
2052
2085
|
#Shared context
|
2053
2086
|
_ctx = None
|
2054
2087
|
|
2055
|
-
def __init__(self, threaded, runargs, resolution=None, binpath=None, context=
|
2088
|
+
def __init__(self, threaded, runargs, resolution=None, binpath=None, context=None):
|
2056
2089
|
self.args = runargs
|
2057
2090
|
self._closing = False
|
2058
2091
|
self.resolution = resolution
|
2059
2092
|
self._loop = None
|
2060
2093
|
|
2061
2094
|
#OpenGL context creation options
|
2095
|
+
if context is None:
|
2096
|
+
context = os.environ.get("LV_CONTEXT", "default")
|
2062
2097
|
havecontext = False
|
2063
2098
|
self.use_moderngl = False
|
2064
2099
|
self.use_moderngl_window = False
|
@@ -2319,11 +2354,10 @@ class _LavaVuWrapper(LavaVuPython.LavaVu):
|
|
2319
2354
|
if _LavaVuWrapper._ctx:
|
2320
2355
|
self.ctx = _LavaVuWrapper._ctx
|
2321
2356
|
else:
|
2322
|
-
import platform
|
2323
2357
|
if platform.system() == 'Linux':
|
2324
2358
|
self.ctx = moderngl.create_context(standalone=True, require=330, backend='egl')
|
2325
2359
|
else:
|
2326
|
-
self.ctx = moderngl.
|
2360
|
+
self.ctx = moderngl.create_context(standalone=True, require=330)
|
2327
2361
|
#print(self.ctx.info)
|
2328
2362
|
_LavaVuWrapper._ctx = self.ctx
|
2329
2363
|
|
@@ -2716,6 +2750,7 @@ class Viewer(dict):
|
|
2716
2750
|
context : str
|
2717
2751
|
OpenGL context type, *"default"* will create a context and window based on available configurations.
|
2718
2752
|
*"provided"* specifies a user provided context, set this if you have already created and activated the context.
|
2753
|
+
*"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
2754
|
*"moderngl"* creates a context in python using the moderngl module (experimental).
|
2720
2755
|
*"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
2756
|
port : int
|
@@ -2735,6 +2770,8 @@ class Viewer(dict):
|
|
2735
2770
|
self._thread = None
|
2736
2771
|
self._collections = {}
|
2737
2772
|
self.validate = True #Property validation flag
|
2773
|
+
self.recording = None
|
2774
|
+
self.context = os.environ.get('LV_CONTEXT', context)
|
2738
2775
|
|
2739
2776
|
#Exit handler to clean up threads
|
2740
2777
|
#(__del__ does not always seem to get called on termination)
|
@@ -2775,7 +2812,7 @@ class Viewer(dict):
|
|
2775
2812
|
if not binpath:
|
2776
2813
|
binpath = os.path.abspath(os.path.dirname(__file__))
|
2777
2814
|
try:
|
2778
|
-
self.app = _LavaVuWrapper(threads, self.args(*args, **kwargs), resolution, binpath, context)
|
2815
|
+
self.app = _LavaVuWrapper(threads, self.args(*args, **kwargs), resolution, binpath, self.context)
|
2779
2816
|
except (RuntimeError) as e:
|
2780
2817
|
print("LavaVu Init error: " + str(e))
|
2781
2818
|
pass
|
@@ -3821,6 +3858,9 @@ class Viewer(dict):
|
|
3821
3858
|
Render a new frame, explicit display update
|
3822
3859
|
"""
|
3823
3860
|
self.app.render()
|
3861
|
+
#Video recording?
|
3862
|
+
if self.recording:
|
3863
|
+
self.recording.frame()
|
3824
3864
|
|
3825
3865
|
def init(self):
|
3826
3866
|
"""
|
@@ -4128,7 +4168,7 @@ class Viewer(dict):
|
|
4128
4168
|
from IPython.display import display,HTML,Javascript
|
4129
4169
|
display(Javascript(js + code))
|
4130
4170
|
|
4131
|
-
def video(self, filename="", fps=30, quality=
|
4171
|
+
def video(self, filename="", resolution=(0,0), fps=30, quality=0, encoder="h264", player=None, options={}, **kwargs):
|
4132
4172
|
"""
|
4133
4173
|
Record and show the generated video inline within an ipython notebook.
|
4134
4174
|
|
@@ -4145,25 +4185,36 @@ class Viewer(dict):
|
|
4145
4185
|
----------
|
4146
4186
|
filename : str
|
4147
4187
|
Name of the file to save, if not provided a default will be used
|
4188
|
+
resolution : list or tuple
|
4189
|
+
Video resolution in pixels [x,y]
|
4148
4190
|
fps : int
|
4149
4191
|
Frames to output per second of video
|
4150
4192
|
quality : int
|
4151
|
-
Encoding quality, 1=low
|
4193
|
+
Encoding quality, 1=low, 2=medium, 3=high, higher quality reduces
|
4152
4194
|
encoding artifacts at cost of larger file size
|
4153
|
-
|
4154
|
-
|
4195
|
+
If omitted will use default settings, can fine tune settings in kwargs
|
4196
|
+
encoder : str
|
4197
|
+
Name of encoder to use, eg: "h264" (default), "mpeg"
|
4198
|
+
player : dict
|
4199
|
+
Args to pass to the player when the video is finished, eg:
|
4200
|
+
{"width" : 800, "height", 400, "params": "controls autoplay"}
|
4201
|
+
options : dict
|
4202
|
+
Args to pass to the encoder, eg: see
|
4203
|
+
https://trac.ffmpeg.org/wiki/Encode/H.264
|
4155
4204
|
**kwargs :
|
4156
|
-
Any additional keyword args will be passed to
|
4205
|
+
Any additional keyword args will also be passed as options to the encoder
|
4157
4206
|
|
4158
4207
|
Returns
|
4159
4208
|
-------
|
4160
4209
|
recorder : Video(object)
|
4161
4210
|
Context manager object that controls the video recording
|
4162
4211
|
"""
|
4163
|
-
return Video(self, filename, resolution, fps, quality, **kwargs)
|
4212
|
+
return Video(self, filename, resolution, fps, quality, encoder, player, options, **kwargs)
|
4164
4213
|
|
4165
4214
|
def video_steps(self, filename="", start=0, end=0, fps=10, quality=1, resolution=(0,0), **kwargs):
|
4166
4215
|
"""
|
4216
|
+
TODO: Fix to use pyAV
|
4217
|
+
|
4167
4218
|
Record a video of the model by looping through all time steps
|
4168
4219
|
|
4169
4220
|
Shows the generated video inline within an ipython notebook.
|
@@ -4357,8 +4408,10 @@ class Viewer(dict):
|
|
4357
4408
|
|
4358
4409
|
Parameters
|
4359
4410
|
----------
|
4360
|
-
resolution : tuple(int,int)
|
4361
|
-
Frame size in pixels: width, height.
|
4411
|
+
resolution : int, tuple(int,int)
|
4412
|
+
Frame size in pixels: width, height.
|
4413
|
+
Defaults to half the viewer default output resolution with a minimum width of 640 pixels
|
4414
|
+
If a single integer is passed, will use as the width while maintaining output aspect ratio
|
4362
4415
|
menu : boolean
|
4363
4416
|
Adds a menu to the top right allowing control of vis parameters, defaults to on
|
4364
4417
|
"""
|
@@ -4376,7 +4429,7 @@ class Viewer(dict):
|
|
4376
4429
|
#Issue redisplay to active viewer
|
4377
4430
|
self.control.redisplay()
|
4378
4431
|
|
4379
|
-
def camera(self, data=None):
|
4432
|
+
def camera(self, data=None, quiet=False):
|
4380
4433
|
"""
|
4381
4434
|
Get/set the current camera viewpoint
|
4382
4435
|
|
@@ -4387,6 +4440,8 @@ class Viewer(dict):
|
|
4387
4440
|
----------
|
4388
4441
|
data : dict
|
4389
4442
|
Camera view to apply if any
|
4443
|
+
quiet : bool
|
4444
|
+
Skip print and calling "camera" script command which produces noisy terminal output
|
4390
4445
|
|
4391
4446
|
Returns
|
4392
4447
|
-------
|
@@ -4397,7 +4452,8 @@ class Viewer(dict):
|
|
4397
4452
|
me = getname(self)
|
4398
4453
|
if not me: me = "lv"
|
4399
4454
|
#Also print in terminal for debugging
|
4400
|
-
|
4455
|
+
if not quiet and not data:
|
4456
|
+
self.commands("camera")
|
4401
4457
|
#Get: export from first view
|
4402
4458
|
vdat = {}
|
4403
4459
|
if len(self.state["views"]) and self.state["views"][0]:
|
@@ -4415,8 +4471,9 @@ class Viewer(dict):
|
|
4415
4471
|
|
4416
4472
|
copyview(vdat, self.state["views"][0])
|
4417
4473
|
|
4418
|
-
|
4419
|
-
|
4474
|
+
if not quiet and not data:
|
4475
|
+
print(me + ".translation(" + str(vdat["translate"])[1:-1] + ")")
|
4476
|
+
print(me + ".rotation(" + str(vdat["xyzrotate"])[1:-1] + ")")
|
4420
4477
|
|
4421
4478
|
#Set
|
4422
4479
|
if data is not None:
|
@@ -4426,6 +4483,87 @@ class Viewer(dict):
|
|
4426
4483
|
#Return
|
4427
4484
|
return vdat
|
4428
4485
|
|
4486
|
+
def lookat(self, pos, at=None, up=None):
|
4487
|
+
"""
|
4488
|
+
Set the camera with a position coord and lookat coord
|
4489
|
+
|
4490
|
+
Parameters
|
4491
|
+
----------
|
4492
|
+
pos : list/numpy.ndarray
|
4493
|
+
Camera position in world coords
|
4494
|
+
lookat : list/numpy.ndarray
|
4495
|
+
Look at position in world coords, defaults to model origin
|
4496
|
+
up : list/numpy.ndarray
|
4497
|
+
Up vector, defaults to Y axis [0,1,0]
|
4498
|
+
"""
|
4499
|
+
|
4500
|
+
# Use the origin from viewer if no target provided
|
4501
|
+
if at is None:
|
4502
|
+
at = self["focus"]
|
4503
|
+
else:
|
4504
|
+
self["focus"] = at
|
4505
|
+
|
4506
|
+
# Default to Y-axis up vector
|
4507
|
+
if up is None:
|
4508
|
+
up = numpy.array([0, 1, 0])
|
4509
|
+
|
4510
|
+
# Calculate the rotation matrix
|
4511
|
+
heading = numpy.array(pos) - numpy.array(at)
|
4512
|
+
zd = vector_normalise(heading)
|
4513
|
+
xd = vector_normalise(numpy.cross(up, zd))
|
4514
|
+
yd = vector_normalise(numpy.cross(zd, xd))
|
4515
|
+
q = quat.from_rotation_matrix(numpy.array([xd, yd, zd]))
|
4516
|
+
q = q.normalized()
|
4517
|
+
|
4518
|
+
# Apply the rotation
|
4519
|
+
self.rotation(q.x, q.y, q.z, q.w)
|
4520
|
+
|
4521
|
+
# Translate back by heading vector length in Z
|
4522
|
+
# (model origin in lavavu takes care of lookat offset)
|
4523
|
+
tr = [0, 0, -vector_magnitude(numpy.array(pos) - numpy.array(at))]
|
4524
|
+
|
4525
|
+
# Apply translation
|
4526
|
+
self.translation(tr)
|
4527
|
+
|
4528
|
+
def camlerpto(self, pos, L):
|
4529
|
+
# Lerp using current camera orientation as start point
|
4530
|
+
pos0 = self.camera(quiet=True)
|
4531
|
+
return self.camlerp(pos0, pos)
|
4532
|
+
|
4533
|
+
def camlerp(self, pos0, pos1, L):
|
4534
|
+
"""
|
4535
|
+
Linearly Interpolate between two camera positions/orientations and
|
4536
|
+
set the camera to the resulting position/orientation
|
4537
|
+
"""
|
4538
|
+
final = {}
|
4539
|
+
for key in ["translate", "rotate", "focus"]:
|
4540
|
+
val0 = numpy.array(pos0[key])
|
4541
|
+
val1 = numpy.array(pos1[key])
|
4542
|
+
res = val0 + (val1 - val0) * L
|
4543
|
+
if len(res) > 3:
|
4544
|
+
# Normalise quaternion
|
4545
|
+
res = res / numpy.linalg.norm(res)
|
4546
|
+
final[key] = res.tolist()
|
4547
|
+
|
4548
|
+
self.camera(final)
|
4549
|
+
|
4550
|
+
def flyto(self, pos, steps, stop=False, callback=None):
|
4551
|
+
# Fly using current camera orientation as start point
|
4552
|
+
pos0 = self.camera(quiet=True)
|
4553
|
+
return self.fly(pos0, pos, steps, stop, callback)
|
4554
|
+
|
4555
|
+
def fly(self, pos0, pos1, steps, stop=False, callback=None):
|
4556
|
+
self.camera(pos0)
|
4557
|
+
self.render()
|
4558
|
+
|
4559
|
+
for i in range(steps):
|
4560
|
+
if stop and i > stop:
|
4561
|
+
break
|
4562
|
+
L = i / (steps - 1)
|
4563
|
+
self.camlerp(pos0, pos1, L)
|
4564
|
+
if callback is not None:
|
4565
|
+
callback(self, i, steps)
|
4566
|
+
self.render()
|
4429
4567
|
|
4430
4568
|
def getview(self):
|
4431
4569
|
"""
|
@@ -5267,10 +5405,8 @@ def player(filename, params="controls autoplay loop", **kwargs):
|
|
5267
5405
|
return filename
|
5268
5406
|
''';
|
5269
5407
|
|
5270
|
-
import uuid
|
5271
|
-
uid = uuid.uuid1()
|
5272
|
-
|
5273
5408
|
#Embed player
|
5409
|
+
filename = os.path.relpath(filename)
|
5274
5410
|
display(Video(url=filename, html_attributes=f"id='{vid}' " + params, **kwargs))
|
5275
5411
|
|
5276
5412
|
#Add download link
|
@@ -5303,6 +5439,8 @@ class Video(object):
|
|
5303
5439
|
"""
|
5304
5440
|
The Video class provides an interface to record animations
|
5305
5441
|
|
5442
|
+
This now uses pyAV avoiding the need to build LavaVu with ffmpeg support
|
5443
|
+
|
5306
5444
|
Example
|
5307
5445
|
-------
|
5308
5446
|
|
@@ -5314,7 +5452,7 @@ class Video(object):
|
|
5314
5452
|
... lv.rotate('y', 10) # doctest: +SKIP
|
5315
5453
|
... lv.render() # doctest: +SKIP
|
5316
5454
|
"""
|
5317
|
-
def __init__(self, viewer=None, filename="", resolution=(0,0), framerate=30, quality=
|
5455
|
+
def __init__(self, viewer=None, filename="", resolution=(0,0), framerate=30, quality=0, encoder="h264", player=None, options={}, **kwargs):
|
5318
5456
|
"""
|
5319
5457
|
Record and show the generated video inline within an ipython notebook.
|
5320
5458
|
|
@@ -5334,79 +5472,141 @@ class Video(object):
|
|
5334
5472
|
fps : int
|
5335
5473
|
Frames to output per second of video
|
5336
5474
|
quality : int
|
5337
|
-
Encoding quality, 1=low
|
5475
|
+
Encoding quality, 1=low, 2=medium, 3=high, higher quality reduces
|
5338
5476
|
encoding artifacts at cost of larger file size
|
5477
|
+
If omitted will use default settings, can fine tune settings in kwargs
|
5339
5478
|
resolution : list or tuple
|
5340
5479
|
Video resolution in pixels [x,y]
|
5480
|
+
encoder : str
|
5481
|
+
Name of encoder to use, eg: "h264" (default), "mpeg"
|
5482
|
+
player : dict
|
5483
|
+
Args to pass to the player when the video is finished, eg:
|
5484
|
+
{"width" : 800, "height", 400, "params": "controls autoplay"}
|
5485
|
+
options : dict
|
5486
|
+
Args to pass to the encoder, eg: see
|
5487
|
+
https://trac.ffmpeg.org/wiki/Encode/H.264
|
5341
5488
|
**kwargs :
|
5342
|
-
Any additional keyword args will be passed to
|
5489
|
+
Any additional keyword args will also be passed as options to the encoder
|
5343
5490
|
"""
|
5491
|
+
if av is None:
|
5492
|
+
raise(ImportError("Video output not supported without pyAV - pip install av"))
|
5493
|
+
return
|
5344
5494
|
self.resolution = resolution
|
5495
|
+
if self.resolution[0] == 0:
|
5496
|
+
self.resolution = viewer.width
|
5497
|
+
if self.resolution[1] == 0:
|
5498
|
+
self.resolution = viewer.height
|
5345
5499
|
self.framerate = framerate
|
5346
5500
|
self.quality = quality
|
5347
5501
|
self.viewer = viewer
|
5348
5502
|
self.filename = filename
|
5349
|
-
self.
|
5350
|
-
|
5351
|
-
|
5352
|
-
|
5353
|
-
|
5503
|
+
if len(self.filename) == 0:
|
5504
|
+
self.filename = "lavavu.mp4"
|
5505
|
+
self.player = player
|
5506
|
+
if self.player is None:
|
5507
|
+
#Default player is half output resolution
|
5508
|
+
self.player = {"width": self.resolution[0] // 2, "height": self.resolution[1] // 2}
|
5509
|
+
self.encoder = encoder
|
5510
|
+
self.options = options
|
5511
|
+
#Also include extra args
|
5512
|
+
options.update(kwargs)
|
5513
|
+
self.container = None
|
5354
5514
|
|
5355
5515
|
def start(self):
|
5356
5516
|
"""
|
5357
5517
|
Start recording, all rendered frames will be added to the video
|
5358
5518
|
"""
|
5359
|
-
|
5360
|
-
|
5361
|
-
|
5362
|
-
|
5363
|
-
|
5364
|
-
|
5365
|
-
|
5366
|
-
|
5367
|
-
|
5368
|
-
|
5519
|
+
#https://trac.ffmpeg.org/wiki/Encode/H.264
|
5520
|
+
# The range of the CRF scale is 0–51, where 0 is lossless (for 8 bit only, for 10 bit use -qp 0),
|
5521
|
+
# 23 is the default, and 51 is worst quality possible
|
5522
|
+
#Compression level, lower = high quality
|
5523
|
+
#See also: https://github.com/PyAV-Org/PyAV/blob/main/tests/test_encode.py
|
5524
|
+
options = {}
|
5525
|
+
if self.encoder == 'h264' or self.encoder == 'libx265':
|
5526
|
+
#Only have default options for h264/265 for now
|
5527
|
+
if self.quality == 1:
|
5528
|
+
options['qmin'] = '30' #'20' #'8'
|
5529
|
+
options['qmax'] = '35' #'41'
|
5530
|
+
options['crf'] = '30' #'40'
|
5531
|
+
elif self.quality == 2:
|
5532
|
+
options['qmin'] = '25' #'2'
|
5533
|
+
options['qmax'] = '30' #'31'
|
5534
|
+
options['crf'] = '20' #'23'
|
5535
|
+
elif self.quality == 3:
|
5536
|
+
options['qmin'] = '20' #'1'
|
5537
|
+
options['qmax'] = '25' #'4'
|
5538
|
+
options['crf'] = '10' #'10'
|
5539
|
+
|
5540
|
+
#Default preset, tune for h264
|
5541
|
+
options["preset"] = 'veryfast' #'veryfast' 'medium' 'slow'
|
5542
|
+
options["tune"] = 'animation' #'film'
|
5543
|
+
|
5544
|
+
#Settings from our original c++ encoder
|
5545
|
+
#self.options['i_quant_factor'] = '0.71'
|
5546
|
+
#self.options['qcompress'] = '0.6'
|
5547
|
+
#self.options['max_qdiff'] = '4'
|
5548
|
+
#self.options['refs'] = '3'
|
5549
|
+
|
5550
|
+
#Merge user options, allowing override of above settings
|
5551
|
+
options.update(self.options)
|
5552
|
+
|
5553
|
+
#print(options)
|
5554
|
+
self.container = av.open(self.filename, mode="w")
|
5555
|
+
self.stream = self.container.add_stream(self.encoder, rate=self.framerate, options=options)
|
5556
|
+
self.stream.width = self.resolution[0]
|
5557
|
+
self.stream.height = self.resolution[1]
|
5558
|
+
self.stream.pix_fmt = "yuv420p"
|
5559
|
+
self.viewer.recording = self
|
5560
|
+
|
5561
|
+
stream = self.stream
|
5562
|
+
#print(stream)
|
5563
|
+
#print(stream.codec)
|
5564
|
+
#print(stream.codec_context)
|
5565
|
+
#print(stream.profiles)
|
5566
|
+
#print(stream.profile)
|
5567
|
+
#print(stream.options)
|
5568
|
+
#Need to set profile here or it isn't applied
|
5569
|
+
#(Default to main)
|
5570
|
+
stream.profile = 'Main' #Baseline / High
|
5571
|
+
cc = stream.codec_context
|
5572
|
+
#print(cc.options)
|
5573
|
+
#print(cc.profile)
|
5574
|
+
|
5575
|
+
def frame(self):
|
5576
|
+
"""
|
5577
|
+
Write a frame, called when viewer.render() is called
|
5578
|
+
while a recording is in progress
|
5579
|
+
"""
|
5580
|
+
img = self.viewer.rawimage(resolution=self.resolution, channels=3)
|
5581
|
+
frame = av.VideoFrame.from_ndarray(img.data, format="rgb24")
|
5582
|
+
for packet in self.stream.encode(frame):
|
5583
|
+
self.container.mux(packet)
|
5369
5584
|
|
5370
5585
|
def pause(self):
|
5371
5586
|
"""
|
5372
5587
|
Pause/resume recording, no rendered frames will be added to the video while paused
|
5373
5588
|
"""
|
5374
|
-
if self.
|
5375
|
-
self.
|
5589
|
+
if self.viewer.recording:
|
5590
|
+
self.viewer.recording = None
|
5376
5591
|
else:
|
5377
|
-
self.viewer.
|
5592
|
+
self.viewer.recording = self
|
5378
5593
|
|
5379
5594
|
def stop(self):
|
5380
5595
|
"""
|
5381
5596
|
Stop recording, final frames will be written and file closed, ready to play.
|
5382
5597
|
No further frames will be added to the video
|
5383
5598
|
"""
|
5384
|
-
|
5385
|
-
|
5386
|
-
self.
|
5387
|
-
|
5388
|
-
|
5389
|
-
|
5390
|
-
|
5391
|
-
if os.path.isdir(self.filename):
|
5392
|
-
log = ""
|
5393
|
-
ffmpeg_exe = 'ffmpeg'
|
5394
|
-
try:
|
5395
|
-
outfn = '{0}.mp4'.format(self.filename)
|
5396
|
-
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)
|
5397
|
-
log += "No built in video encoding, attempting to build movie from frames with ffmpeg:\n"
|
5398
|
-
log += cmd
|
5399
|
-
|
5400
|
-
import subprocess
|
5401
|
-
#os.system(cmd)
|
5402
|
-
subprocess.check_call(cmd, shell=True)
|
5403
|
-
self.filename = outfn
|
5404
|
-
except (Exception) as e:
|
5405
|
-
print("Video encoding failed: ", str(e), "\nlog:\n", log)
|
5599
|
+
# Flush stream
|
5600
|
+
for packet in self.stream.encode():
|
5601
|
+
self.container.mux(packet)
|
5602
|
+
# Close the file
|
5603
|
+
self.container.close()
|
5604
|
+
self.container = None
|
5605
|
+
self.stream = None
|
5406
5606
|
|
5407
5607
|
def write(self, image):
|
5408
5608
|
"""
|
5409
|
-
Add a frame to the video
|
5609
|
+
Add a frame to the video
|
5410
5610
|
|
5411
5611
|
Parameters
|
5412
5612
|
----------
|
@@ -5414,7 +5614,7 @@ class Video(object):
|
|
5414
5614
|
Pass a list or numpy uint8 array of rgba values or an Image object
|
5415
5615
|
values are loaded as 8 bit unsigned integer values
|
5416
5616
|
"""
|
5417
|
-
if self.
|
5617
|
+
if self.container:
|
5418
5618
|
if isinstance(image, Image):
|
5419
5619
|
image = image.data
|
5420
5620
|
else:
|
@@ -5422,13 +5622,18 @@ class Video(object):
|
|
5422
5622
|
if image.size == self.resolution[0] * self.resolution[1] * 4:
|
5423
5623
|
image = image.reshape(self.resolution[0], self.resolution[1], 4)
|
5424
5624
|
image = image[::,::,:3] #Remove alpha channel
|
5425
|
-
self.encoder.copyframe(image.ravel())
|
5625
|
+
#self.encoder.copyframe(image.ravel())
|
5626
|
+
|
5627
|
+
frame = av.VideoFrame.from_ndarray(image, format="rgb24")
|
5628
|
+
for packet in self.stream.encode(frame):
|
5629
|
+
self.container.mux(packet)
|
5426
5630
|
|
5427
5631
|
def play(self):
|
5428
5632
|
"""
|
5429
5633
|
Show the video in an inline player if in an interative notebook
|
5430
5634
|
"""
|
5431
|
-
|
5635
|
+
print(self.player)
|
5636
|
+
player(self.filename, **self.player)
|
5432
5637
|
|
5433
5638
|
def __enter__(self):
|
5434
5639
|
self.start()
|
@@ -5847,6 +6052,56 @@ def lerp(first, second, mu):
|
|
5847
6052
|
final[i] += diff * mu
|
5848
6053
|
return final
|
5849
6054
|
|
6055
|
+
def vector_magnitude(vec):
|
6056
|
+
return numpy.linalg.norm(vec)
|
6057
|
+
|
6058
|
+
def vector_normalise(vec):
|
6059
|
+
norm = numpy.linalg.norm(vec)
|
6060
|
+
if norm == 0:
|
6061
|
+
vn = vec
|
6062
|
+
else:
|
6063
|
+
vn = vec / norm
|
6064
|
+
return vn
|
6065
|
+
|
6066
|
+
def vector_align(v1, v2, lvformat=True):
|
6067
|
+
"""
|
6068
|
+
Get a rotation quaterion to align vectors v1 with v2
|
6069
|
+
|
6070
|
+
Parameters
|
6071
|
+
----------
|
6072
|
+
v1 : list/numpy.ndarray
|
6073
|
+
First 3 component vector
|
6074
|
+
v2 : list/numpy.ndarray
|
6075
|
+
Second 3 component vector to align the first to
|
6076
|
+
|
6077
|
+
Returns
|
6078
|
+
-------
|
6079
|
+
list: quaternion to rotate v1 to v2 (in lavavu format)
|
6080
|
+
"""
|
6081
|
+
|
6082
|
+
# Check for parallel or opposite
|
6083
|
+
v1 = vector_normalise(numpy.array(v1))
|
6084
|
+
v2 = vector_normalise(numpy.array(v2))
|
6085
|
+
epsilon = numpy.finfo(numpy.float32).eps
|
6086
|
+
one_minus_eps = 1.0 - epsilon
|
6087
|
+
if numpy.dot(v1, v2) > one_minus_eps: # 1.0
|
6088
|
+
# No rotation
|
6089
|
+
return [0, 0, 0, 1]
|
6090
|
+
elif numpy.dot(v1, v2) < -one_minus_eps: # -1.0
|
6091
|
+
# 180 rotation about Y
|
6092
|
+
return [0, 1, 0, 1]
|
6093
|
+
xyz = numpy.cross(v1, v2)
|
6094
|
+
l1 = numpy.linalg.norm(v1)
|
6095
|
+
l2 = numpy.linalg.norm(v2)
|
6096
|
+
w = math.sqrt((l1 * l1) * (l2 * l2)) + numpy.dot(v1, v2)
|
6097
|
+
qr = quat.quaternion(w, xyz[0], xyz[1], xyz[2])
|
6098
|
+
qr = qr.normalized()
|
6099
|
+
# Return in LavaVu quaternion format
|
6100
|
+
if lvformat:
|
6101
|
+
return [qr.x, qr.y, qr.z, qr.w]
|
6102
|
+
else:
|
6103
|
+
return qr
|
6104
|
+
|
5850
6105
|
if __name__ == '__main__':
|
5851
6106
|
#Run doctests - only works on numpy 1.14+ due to changes in array printing
|
5852
6107
|
npyv = numpy.__version__.split('.')
|
@@ -5856,3 +6111,4 @@ if __name__ == '__main__':
|
|
5856
6111
|
import doctest
|
5857
6112
|
doctest.testmod()
|
5858
6113
|
|
6114
|
+
|