lavavu 1.8.83__cp310-cp310-manylinux_2_28_x86_64.whl → 1.9.0__cp310-cp310-manylinux_2_28_x86_64.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.
Files changed (43) hide show
  1. lavavu/_LavaVuPython.cpython-310-x86_64-linux-gnu.so +0 -0
  2. lavavu/control.py +14 -2
  3. lavavu/html/webview.html +1 -1
  4. lavavu/lavavu.py +324 -68
  5. lavavu/osmesa/LavaVuPython.py +561 -0
  6. lavavu/osmesa/_LavaVuPython.cpython-310-x86_64-linux-gnu.so +0 -0
  7. lavavu/osmesa/__init__.py +0 -0
  8. lavavu/shaders/default.frag +0 -6
  9. lavavu/shaders/default.vert +4 -4
  10. lavavu/shaders/fontShader.frag +0 -5
  11. lavavu/shaders/lineShader.frag +0 -4
  12. lavavu/shaders/lineShader.vert +0 -2
  13. lavavu/shaders/pointShader.frag +0 -5
  14. lavavu/shaders/pointShader.vert +0 -4
  15. lavavu/shaders/triShader.frag +0 -17
  16. lavavu/shaders/triShader.vert +0 -4
  17. lavavu/shaders/volumeShader.frag +0 -63
  18. {lavavu-1.8.83.dist-info → lavavu-1.9.0.dist-info}/METADATA +5 -4
  19. lavavu-1.9.0.dist-info/RECORD +65 -0
  20. {lavavu-1.8.83.dist-info → lavavu-1.9.0.dist-info}/WHEEL +1 -1
  21. lavavu.libs/libLLVM-17-51492e70.so +0 -0
  22. lavavu.libs/libOSMesa-25f49adf.so.8.0.0 +0 -0
  23. lavavu.libs/libdrm-b0291a67.so.2.4.0 +0 -0
  24. lavavu.libs/libffi-3a37023a.so.6.0.2 +0 -0
  25. lavavu.libs/libglapi-d9260dde.so.0.0.0 +0 -0
  26. lavavu.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
  27. lavavu.libs/libselinux-64a010fa.so.1 +0 -0
  28. lavavu.libs/libtinfo-3a2cb85b.so.6.1 +0 -0
  29. lavavu.libs/libzstd-76b78bac.so.1.4.4 +0 -0
  30. lavavu-1.8.83.dist-info/RECORD +0 -63
  31. lavavu.libs/libavcodec-c07e82e9.so.61.24.100 +0 -0
  32. lavavu.libs/libavformat-2c4b075e.so.61.9.100 +0 -0
  33. lavavu.libs/libavutil-1e63d46c.so.59.46.100 +0 -0
  34. lavavu.libs/libbz2-e34b29ae.so.1.0.6 +0 -0
  35. lavavu.libs/libjbig-2504a0c3.so.2.1 +0 -0
  36. lavavu.libs/libjpeg-da649728.so.62.2.0 +0 -0
  37. lavavu.libs/libswresample-0ba304d7.so.5.4.100 +0 -0
  38. lavavu.libs/libswscale-4ff68837.so.8.9.101 +0 -0
  39. lavavu.libs/libtiff-97fb0e7a.so.5.3.0 +0 -0
  40. lavavu.libs/libx264-6f5370e2.so.164 +0 -0
  41. {lavavu-1.8.83.dist-info → lavavu-1.9.0.dist-info}/LICENSE.md +0 -0
  42. {lavavu-1.8.83.dist-info → lavavu-1.9.0.dist-info}/entry_points.txt +0 -0
  43. {lavavu-1.8.83.dist-info → lavavu-1.9.0.dist-info}/top_level.txt +0 -0
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 not None:
485
- viewer.output_resolution = resolution
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.8.83/LavaVu-amalgamated.min.js"></script>
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', 'lerp',
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
- import LavaVuPython
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="default"):
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.create_standalone_context(require=330)
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=1, resolution=(0,0), **kwargs):
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(default), 2=medium, 3=high, higher quality reduces
4193
+ Encoding quality, 1=low, 2=medium, 3=high, higher quality reduces
4152
4194
  encoding artifacts at cost of larger file size
4153
- resolution : list or tuple
4154
- Video resolution in pixels [x,y]
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 lavavu.player()
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. Defaults to the viewer default output resolution, usually (640,480)
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
- self.commands("camera")
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
- print(me + ".translation(" + str(vdat["translate"])[1:-1] + ")")
4419
- print(me + ".rotation(" + str(vdat["xyzrotate"])[1:-1] + ")")
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=1, **kwargs):
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(default), 2=medium, 3=high, higher quality reduces
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 lavavu.player()
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.kwargs = kwargs
5350
- if not viewer:
5351
- self.encoder = LavaVuPython.VideoEncoder(filename, framerate, quality);
5352
- else:
5353
- self.encoder = None
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
- if self.encoder:
5360
- if self.resolution[0] <= 0: self.resolution[0] = 1280
5361
- if self.resolution[1] <= 0: self.resolution[1] = 720
5362
- self.encoder.open(self.resolution[0], self.resolution[1])
5363
- else:
5364
- self.filename = self.viewer.app.encodeVideo(self.filename, self.framerate, self.quality, self.resolution[0], self.resolution[1])
5365
- #Clear existing image frames
5366
- if os.path.isdir(self.filename):
5367
- for f in glob.glob(self.filename + "/frame_*.jpg"):
5368
- os.remove(f)
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.encoder:
5375
- self.encoder.render = not self.encoder.render
5589
+ if self.viewer.recording:
5590
+ self.viewer.recording = None
5376
5591
  else:
5377
- self.viewer.app.pauseVideo()
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
- if self.encoder:
5385
- self.encoder.close()
5386
- self.filename = self.encoder.filename
5387
- else:
5388
- self.viewer.app.encodeVideo()
5389
- #Check if encoded video is a directory (not built with video encoding support)
5390
- #if so attempt to encode with ffmpeg
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 (when writing custom video frames rather than rendering them within lavavu)
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.encoder:
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
- player(self.filename, **self.kwargs)
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
+