lavavu 1.8.84__cp38-cp38-manylinux_2_28_x86_64.whl → 1.9.0__cp38-cp38-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 (42) hide show
  1. lavavu/_LavaVuPython.cpython-38-x86_64-linux-gnu.so +0 -0
  2. lavavu/html/webview.html +1 -1
  3. lavavu/lavavu.py +322 -67
  4. lavavu/osmesa/LavaVuPython.py +561 -0
  5. lavavu/osmesa/_LavaVuPython.cpython-38-x86_64-linux-gnu.so +0 -0
  6. lavavu/osmesa/__init__.py +0 -0
  7. lavavu/shaders/default.frag +0 -6
  8. lavavu/shaders/default.vert +4 -4
  9. lavavu/shaders/fontShader.frag +0 -5
  10. lavavu/shaders/lineShader.frag +0 -4
  11. lavavu/shaders/lineShader.vert +0 -2
  12. lavavu/shaders/pointShader.frag +0 -5
  13. lavavu/shaders/pointShader.vert +0 -4
  14. lavavu/shaders/triShader.frag +0 -17
  15. lavavu/shaders/triShader.vert +0 -4
  16. lavavu/shaders/volumeShader.frag +0 -63
  17. {lavavu-1.8.84.dist-info → lavavu-1.9.0.dist-info}/METADATA +3 -2
  18. lavavu-1.9.0.dist-info/RECORD +65 -0
  19. lavavu.libs/libLLVM-17-51492e70.so +0 -0
  20. lavavu.libs/libOSMesa-25f49adf.so.8.0.0 +0 -0
  21. lavavu.libs/libdrm-b0291a67.so.2.4.0 +0 -0
  22. lavavu.libs/libffi-3a37023a.so.6.0.2 +0 -0
  23. lavavu.libs/libglapi-d9260dde.so.0.0.0 +0 -0
  24. lavavu.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
  25. lavavu.libs/libselinux-64a010fa.so.1 +0 -0
  26. lavavu.libs/libtinfo-3a2cb85b.so.6.1 +0 -0
  27. lavavu.libs/libzstd-76b78bac.so.1.4.4 +0 -0
  28. lavavu-1.8.84.dist-info/RECORD +0 -63
  29. lavavu.libs/libavcodec-e9f4b4d9.so.61.24.100 +0 -0
  30. lavavu.libs/libavformat-dd2ce9c7.so.61.9.100 +0 -0
  31. lavavu.libs/libavutil-54907a11.so.59.46.100 +0 -0
  32. lavavu.libs/libbz2-e34b29ae.so.1.0.6 +0 -0
  33. lavavu.libs/libjbig-2504a0c3.so.2.1 +0 -0
  34. lavavu.libs/libjpeg-da649728.so.62.2.0 +0 -0
  35. lavavu.libs/libswresample-3aad7a38.so.5.4.100 +0 -0
  36. lavavu.libs/libswscale-4ff68837.so.8.9.101 +0 -0
  37. lavavu.libs/libtiff-5faff81f.so.5.3.0 +0 -0
  38. lavavu.libs/libx264-6f5370e2.so.164 +0 -0
  39. {lavavu-1.8.84.dist-info → lavavu-1.9.0.dist-info}/LICENSE.md +0 -0
  40. {lavavu-1.8.84.dist-info → lavavu-1.9.0.dist-info}/WHEEL +0 -0
  41. {lavavu-1.8.84.dist-info → lavavu-1.9.0.dist-info}/entry_points.txt +0 -0
  42. {lavavu-1.8.84.dist-info → lavavu-1.9.0.dist-info}/top_level.txt +0 -0
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.84/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.
@@ -4378,7 +4429,7 @@ class Viewer(dict):
4378
4429
  #Issue redisplay to active viewer
4379
4430
  self.control.redisplay()
4380
4431
 
4381
- def camera(self, data=None):
4432
+ def camera(self, data=None, quiet=False):
4382
4433
  """
4383
4434
  Get/set the current camera viewpoint
4384
4435
 
@@ -4389,6 +4440,8 @@ class Viewer(dict):
4389
4440
  ----------
4390
4441
  data : dict
4391
4442
  Camera view to apply if any
4443
+ quiet : bool
4444
+ Skip print and calling "camera" script command which produces noisy terminal output
4392
4445
 
4393
4446
  Returns
4394
4447
  -------
@@ -4399,7 +4452,8 @@ class Viewer(dict):
4399
4452
  me = getname(self)
4400
4453
  if not me: me = "lv"
4401
4454
  #Also print in terminal for debugging
4402
- self.commands("camera")
4455
+ if not quiet and not data:
4456
+ self.commands("camera")
4403
4457
  #Get: export from first view
4404
4458
  vdat = {}
4405
4459
  if len(self.state["views"]) and self.state["views"][0]:
@@ -4417,8 +4471,9 @@ class Viewer(dict):
4417
4471
 
4418
4472
  copyview(vdat, self.state["views"][0])
4419
4473
 
4420
- print(me + ".translation(" + str(vdat["translate"])[1:-1] + ")")
4421
- 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] + ")")
4422
4477
 
4423
4478
  #Set
4424
4479
  if data is not None:
@@ -4428,6 +4483,88 @@ class Viewer(dict):
4428
4483
  #Return
4429
4484
  return vdat
4430
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()
4567
+
4431
4568
  def getview(self):
4432
4569
  """
4433
4570
  Get current view settings
@@ -5268,11 +5405,9 @@ def player(filename, params="controls autoplay loop", **kwargs):
5268
5405
  return filename
5269
5406
  ''';
5270
5407
 
5271
- import uuid
5272
- uid = uuid.uuid1()
5273
-
5274
5408
  #Embed player
5275
- display(Video(url=os.path.relpath(filename), html_attributes=f"id='{vid}' " + params, **kwargs))
5409
+ filename = os.path.relpath(filename)
5410
+ display(Video(url=filename, html_attributes=f"id='{vid}' " + params, **kwargs))
5276
5411
 
5277
5412
  #Add download link
5278
5413
  display(HTML(f'<a id="link_{vid}" href="{filename}" download>Download Video</a>'))
@@ -5304,6 +5439,8 @@ class Video(object):
5304
5439
  """
5305
5440
  The Video class provides an interface to record animations
5306
5441
 
5442
+ This now uses pyAV avoiding the need to build LavaVu with ffmpeg support
5443
+
5307
5444
  Example
5308
5445
  -------
5309
5446
 
@@ -5315,7 +5452,7 @@ class Video(object):
5315
5452
  ... lv.rotate('y', 10) # doctest: +SKIP
5316
5453
  ... lv.render() # doctest: +SKIP
5317
5454
  """
5318
- 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):
5319
5456
  """
5320
5457
  Record and show the generated video inline within an ipython notebook.
5321
5458
 
@@ -5335,79 +5472,141 @@ class Video(object):
5335
5472
  fps : int
5336
5473
  Frames to output per second of video
5337
5474
  quality : int
5338
- Encoding quality, 1=low(default), 2=medium, 3=high, higher quality reduces
5475
+ Encoding quality, 1=low, 2=medium, 3=high, higher quality reduces
5339
5476
  encoding artifacts at cost of larger file size
5477
+ If omitted will use default settings, can fine tune settings in kwargs
5340
5478
  resolution : list or tuple
5341
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
5342
5488
  **kwargs :
5343
- Any additional keyword args will be passed to lavavu.player()
5489
+ Any additional keyword args will also be passed as options to the encoder
5344
5490
  """
5491
+ if av is None:
5492
+ raise(ImportError("Video output not supported without pyAV - pip install av"))
5493
+ return
5345
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
5346
5499
  self.framerate = framerate
5347
5500
  self.quality = quality
5348
5501
  self.viewer = viewer
5349
5502
  self.filename = filename
5350
- self.kwargs = kwargs
5351
- if not viewer:
5352
- self.encoder = LavaVuPython.VideoEncoder(filename, framerate, quality);
5353
- else:
5354
- 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
5355
5514
 
5356
5515
  def start(self):
5357
5516
  """
5358
5517
  Start recording, all rendered frames will be added to the video
5359
5518
  """
5360
- if self.encoder:
5361
- if self.resolution[0] <= 0: self.resolution[0] = 1280
5362
- if self.resolution[1] <= 0: self.resolution[1] = 720
5363
- self.encoder.open(self.resolution[0], self.resolution[1])
5364
- else:
5365
- self.filename = self.viewer.app.encodeVideo(self.filename, self.framerate, self.quality, self.resolution[0], self.resolution[1])
5366
- #Clear existing image frames
5367
- if os.path.isdir(self.filename):
5368
- for f in glob.glob(self.filename + "/frame_*.jpg"):
5369
- 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)
5370
5584
 
5371
5585
  def pause(self):
5372
5586
  """
5373
5587
  Pause/resume recording, no rendered frames will be added to the video while paused
5374
5588
  """
5375
- if self.encoder:
5376
- self.encoder.render = not self.encoder.render
5589
+ if self.viewer.recording:
5590
+ self.viewer.recording = None
5377
5591
  else:
5378
- self.viewer.app.pauseVideo()
5592
+ self.viewer.recording = self
5379
5593
 
5380
5594
  def stop(self):
5381
5595
  """
5382
5596
  Stop recording, final frames will be written and file closed, ready to play.
5383
5597
  No further frames will be added to the video
5384
5598
  """
5385
- if self.encoder:
5386
- self.encoder.close()
5387
- self.filename = self.encoder.filename
5388
- else:
5389
- self.viewer.app.encodeVideo()
5390
- #Check if encoded video is a directory (not built with video encoding support)
5391
- #if so attempt to encode with ffmpeg
5392
- if os.path.isdir(self.filename):
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)
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
5407
5606
 
5408
5607
  def write(self, image):
5409
5608
  """
5410
- Add a frame to the video (when writing custom video frames rather than rendering them within lavavu)
5609
+ Add a frame to the video
5411
5610
 
5412
5611
  Parameters
5413
5612
  ----------
@@ -5415,7 +5614,7 @@ class Video(object):
5415
5614
  Pass a list or numpy uint8 array of rgba values or an Image object
5416
5615
  values are loaded as 8 bit unsigned integer values
5417
5616
  """
5418
- if self.encoder:
5617
+ if self.container:
5419
5618
  if isinstance(image, Image):
5420
5619
  image = image.data
5421
5620
  else:
@@ -5423,13 +5622,18 @@ class Video(object):
5423
5622
  if image.size == self.resolution[0] * self.resolution[1] * 4:
5424
5623
  image = image.reshape(self.resolution[0], self.resolution[1], 4)
5425
5624
  image = image[::,::,:3] #Remove alpha channel
5426
- 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)
5427
5630
 
5428
5631
  def play(self):
5429
5632
  """
5430
5633
  Show the video in an inline player if in an interative notebook
5431
5634
  """
5432
- player(self.filename, **self.kwargs)
5635
+ print(self.player)
5636
+ player(self.filename, **self.player)
5433
5637
 
5434
5638
  def __enter__(self):
5435
5639
  self.start()
@@ -5848,6 +6052,56 @@ def lerp(first, second, mu):
5848
6052
  final[i] += diff * mu
5849
6053
  return final
5850
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
+
5851
6105
  if __name__ == '__main__':
5852
6106
  #Run doctests - only works on numpy 1.14+ due to changes in array printing
5853
6107
  npyv = numpy.__version__.split('.')
@@ -5857,3 +6111,4 @@ if __name__ == '__main__':
5857
6111
  import doctest
5858
6112
  doctest.testmod()
5859
6113
 
6114
+