vidformer 0.10.1__py3-none-any.whl → 0.12.0__py3-none-any.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.
vidformer/__init__.py CHANGED
@@ -9,27 +9,28 @@ vidformer-py is a Python 🐍 interface for [vidformer](https://github.com/ixlab
9
9
  * [🧑‍💻 Source Code](https://github.com/ixlab/vidformer/tree/main/vidformer-py/)
10
10
  """
11
11
 
12
- __version__ = "0.10.1"
12
+ __version__ = "0.12.0"
13
13
 
14
14
 
15
- import subprocess
16
- from fractions import Fraction
17
- import random
18
- import time
15
+ import base64
16
+ import gzip
19
17
  import json
20
- import socket
21
- import os
22
18
  import multiprocessing
23
- import uuid
24
- import threading
25
- import gzip
26
- import base64
19
+ import os
20
+ import random
27
21
  import re
22
+ import socket
23
+ import struct
24
+ import subprocess
25
+ import threading
26
+ import time
27
+ import uuid
28
+ from fractions import Fraction
28
29
  from urllib.parse import urlparse
29
30
 
30
- import requests
31
31
  import msgpack
32
32
  import numpy as np
33
+ import requests
33
34
 
34
35
  _in_notebook = False
35
36
  try:
@@ -104,14 +105,14 @@ def _play(namespace, hls_video_url, hls_js_url, method="html", status_url=None):
104
105
  <script src="{hls_js_url}"></script>
105
106
  </head>
106
107
  <body>
107
- <div id="container"></div>
108
+ <div id="container-{namespace}"></div>
108
109
  <script>
109
110
  var statusUrl = '{status_url}';
110
111
  var videoSrc = '{hls_video_url}';
111
112
  var videoNamespace = '{namespace}';
112
113
 
113
114
  function showWaiting() {{
114
- document.getElementById('container').textContent = 'Waiting...';
115
+ document.getElementById('container-{namespace}').textContent = 'Waiting...';
115
116
  pollStatus();
116
117
  }}
117
118
 
@@ -121,7 +122,7 @@ def _play(namespace, hls_video_url, hls_js_url, method="html", status_url=None):
121
122
  .then(r => r.json())
122
123
  .then(res => {{
123
124
  if (res.ready) {{
124
- document.getElementById('container').textContent = '';
125
+ document.getElementById('container-{namespace}').textContent = '';
125
126
  attachHls();
126
127
  }} else {{
127
128
  pollStatus();
@@ -135,7 +136,7 @@ def _play(namespace, hls_video_url, hls_js_url, method="html", status_url=None):
135
136
  }}
136
137
 
137
138
  function attachHls() {{
138
- var container = document.getElementById('container');
139
+ var container = document.getElementById('container-{namespace}');
139
140
  container.textContent = '';
140
141
  var video = document.createElement('video');
141
142
  video.id = 'video-' + videoNamespace;
@@ -182,6 +183,214 @@ def _play(namespace, hls_video_url, hls_js_url, method="html", status_url=None):
182
183
  raise ValueError("Invalid method")
183
184
 
184
185
 
186
+ def _feb_expr_coded_as_scalar(expr) -> bool:
187
+ if type(expr) is tuple:
188
+ expr = list(expr)
189
+ if type(expr) is FilterExpr:
190
+ return False
191
+ if type(expr) is list:
192
+ if len(expr) > 3:
193
+ return False
194
+ else:
195
+ return all([type(x) is int and x >= -(2**15) and x < 2**15 for x in expr])
196
+ else:
197
+ assert type(expr) in [int, float, str, bytes, SourceExpr, bool, list]
198
+ return True
199
+
200
+
201
+ class _FrameExpressionBlock:
202
+ def __init__(self):
203
+ self._functions = []
204
+ self._literals = []
205
+ self._sources = []
206
+ self._kwarg_keys = []
207
+ self._source_fracs = []
208
+ self._exprs = []
209
+ self._frame_exprs = []
210
+
211
+ def __len__(self):
212
+ return len(self._frame_exprs)
213
+
214
+ def insert_expr(self, expr):
215
+ if type(expr) is SourceExpr or type(expr) is FilterExpr:
216
+ return self.insert_frame_expr(expr)
217
+ else:
218
+ return self.insert_data_expr(expr)
219
+
220
+ def insert_data_expr(self, data):
221
+ if type(data) is tuple:
222
+ data = list(data)
223
+ if type(data) is bool:
224
+ self._exprs.append(0x01000000_00000000 | int(data))
225
+ return len(self._exprs) - 1
226
+ elif type(data) is int:
227
+ if data >= -(2**31) and data < 2**31:
228
+ self._exprs.append(data & 0xFFFFFFFF)
229
+ else:
230
+ self._literals.append(_json_arg(data, skip_data_anot=True))
231
+ self._exprs.append(0x40000000_00000000 | len(self._literals) - 1)
232
+ return len(self._exprs) - 1
233
+ elif type(data) is float:
234
+ self._exprs.append(
235
+ 0x02000000_00000000 | int.from_bytes(struct.pack("f", data)[::-1])
236
+ )
237
+ elif type(data) is str:
238
+ self._literals.append(_json_arg(data, skip_data_anot=True))
239
+ self._exprs.append(0x40000000_00000000 | len(self._literals) - 1)
240
+ elif type(data) is bytes:
241
+ self._literals.append(_json_arg(data, skip_data_anot=True))
242
+ self._exprs.append(0x40000000_00000000 | len(self._literals) - 1)
243
+ elif type(data) is list:
244
+ if len(data) == 0:
245
+ self._exprs.append(0x03000000_00000000)
246
+ return len(self._exprs) - 1
247
+ if (
248
+ len(data) == 1
249
+ and type(data[0]) is int
250
+ and data[0] >= -(2**15)
251
+ and data[0] < 2**15
252
+ ):
253
+ self._exprs.append(0x04000000_00000000 | (data[0] & 0xFFFF))
254
+ return len(self._exprs) - 1
255
+ if (
256
+ len(data) == 2
257
+ and type(data[0]) is int
258
+ and data[0] >= -(2**15)
259
+ and data[0] < 2**15
260
+ and type(data[1]) is int
261
+ and data[1] >= -(2**15)
262
+ and data[1] < 2**15
263
+ ):
264
+ self._exprs.append(
265
+ 0x05000000_00000000
266
+ | ((data[0] & 0xFFFF) << 16)
267
+ | (data[1] & 0xFFFF)
268
+ )
269
+ return len(self._exprs) - 1
270
+ if (
271
+ len(data) == 3
272
+ and type(data[0]) is int
273
+ and data[0] >= -(2**15)
274
+ and data[0] < 2**15
275
+ and type(data[1]) is int
276
+ and data[1] >= -(2**15)
277
+ and data[1] < 2**15
278
+ and type(data[2]) is int
279
+ and data[2] >= -(2**15)
280
+ and data[2] < 2**15
281
+ ):
282
+ self._exprs.append(
283
+ 0x06000000_00000000
284
+ | ((data[0] & 0xFFFF) << 32)
285
+ | ((data[1] & 0xFFFF) << 16)
286
+ | (data[2] & 0xFFFF)
287
+ )
288
+ return len(self._exprs) - 1
289
+ out = len(self._exprs)
290
+ member_idxs = []
291
+ for member in data:
292
+ if _feb_expr_coded_as_scalar(member):
293
+ member_idxs.append(None)
294
+ else:
295
+ member_idxs.append(self.insert_data_expr(member))
296
+
297
+ self._exprs.append(0x42000000_00000000 | len(data))
298
+
299
+ for i in range(len(data)):
300
+ if member_idxs[i] is None:
301
+ self.insert_data_expr(data[i])
302
+ else:
303
+ self._exprs.append(0x45000000_00000000 | member_idxs[i])
304
+
305
+ return out
306
+ else:
307
+ raise Exception("Invalid data type")
308
+
309
+ def insert_frame_expr(self, frame):
310
+ if type(frame) is SourceExpr:
311
+ source = frame._source._name
312
+ if source in self._sources:
313
+ source_idx = self._sources.index(source)
314
+ else:
315
+ source_idx = len(self._sources)
316
+ self._sources.append(source)
317
+ if frame._is_iloc:
318
+ self._exprs.append(
319
+ 0x43000000_00000000 | (source_idx << 32) | frame._idx
320
+ )
321
+ else:
322
+ idx = len(self._source_fracs) // 2
323
+ self._source_fracs.append(frame._idx.numerator)
324
+ self._source_fracs.append(frame._idx.denominator)
325
+ self._exprs.append(0x44000000_00000000 | (source_idx << 32) | idx)
326
+ return len(self._exprs) - 1
327
+ elif type(frame) is FilterExpr:
328
+ func = frame._filter._func
329
+ if func in self._functions:
330
+ func_idx = self._functions.index(func)
331
+ else:
332
+ func_idx = len(self._functions)
333
+ self._functions.append(func)
334
+ len_args = len(frame._args)
335
+ len_kwargs = len(frame._kwargs)
336
+
337
+ arg_idxs = []
338
+ for arg in frame._args:
339
+ if _feb_expr_coded_as_scalar(arg):
340
+ arg_idxs.append(None)
341
+ else:
342
+ arg_idxs.append(self.insert_expr(arg))
343
+ kwarg_idxs = {}
344
+ for k, v in frame._kwargs.items():
345
+ if _feb_expr_coded_as_scalar(v):
346
+ kwarg_idxs[k] = None
347
+ else:
348
+ kwarg_idxs[k] = self.insert_expr(v)
349
+
350
+ out_idx = len(self._exprs)
351
+ self._exprs.append(
352
+ 0x41000000_00000000 | (len_args << 24) | (len_kwargs << 16) | func_idx
353
+ )
354
+ for i in range(len_args):
355
+ if arg_idxs[i] is None:
356
+ # It's a scalar
357
+ self.insert_expr(frame._args[i])
358
+ else:
359
+ # It's an expression pointer
360
+ self._exprs.append(0x45000000_00000000 | arg_idxs[i])
361
+ for k, v in frame._kwargs.items():
362
+ if k in self._kwarg_keys:
363
+ k_idx = self._kwarg_keys.index(k)
364
+ else:
365
+ k_idx = len(self._kwarg_keys)
366
+ self._kwarg_keys.append(k)
367
+ self._exprs.append(0x46000000_00000000 | k_idx)
368
+ if kwarg_idxs[k] is None:
369
+ # It's a scalar
370
+ self.insert_expr(v)
371
+ else:
372
+ # It's an expression pointer
373
+ self._exprs.append(0x45000000_00000000 | kwarg_idxs[k])
374
+ return out_idx
375
+ else:
376
+ raise Exception("Invalid frame type")
377
+
378
+ def insert_frame(self, frame):
379
+ idx = self.insert_frame_expr(frame)
380
+ self._frame_exprs.append(idx)
381
+
382
+ def as_dict(self):
383
+ return {
384
+ "functions": self._functions,
385
+ "literals": self._literals,
386
+ "sources": self._sources,
387
+ "kwarg_keys": self._kwarg_keys,
388
+ "source_fracs": self._source_fracs,
389
+ "exprs": self._exprs,
390
+ "frame_exprs": self._frame_exprs,
391
+ }
392
+
393
+
185
394
  class IgniSource:
186
395
  def __init__(self, id: str, src):
187
396
  self._name = id
@@ -245,7 +454,9 @@ class IgniServer:
245
454
  self._endpoint = endpoint
246
455
 
247
456
  self._api_key = api_key
248
- response = requests.get(
457
+ self._session = requests.Session()
458
+ self._session.headers.update({"Authorization": f"Bearer {self._api_key}"})
459
+ response = self._session.get(
249
460
  f"{self._endpoint}/auth",
250
461
  headers={"Authorization": f"Bearer {self._api_key}"},
251
462
  )
@@ -256,7 +467,7 @@ class IgniServer:
256
467
 
257
468
  def get_source(self, id: str) -> IgniSource:
258
469
  assert type(id) is str
259
- response = requests.get(
470
+ response = self._session.get(
260
471
  f"{self._endpoint}/source/{id}",
261
472
  headers={"Authorization": f"Bearer {self._api_key}"},
262
473
  )
@@ -266,7 +477,7 @@ class IgniServer:
266
477
  return IgniSource(response["id"], response)
267
478
 
268
479
  def list_sources(self) -> list[str]:
269
- response = requests.get(
480
+ response = self._session.get(
270
481
  f"{self._endpoint}/source",
271
482
  headers={"Authorization": f"Bearer {self._api_key}"},
272
483
  )
@@ -277,7 +488,7 @@ class IgniServer:
277
488
 
278
489
  def delete_source(self, id: str):
279
490
  assert type(id) is str
280
- response = requests.delete(
491
+ response = self._session.delete(
281
492
  f"{self._endpoint}/source/{id}",
282
493
  headers={"Authorization": f"Bearer {self._api_key}"},
283
494
  )
@@ -302,7 +513,7 @@ class IgniServer:
302
513
  "storage_service": storage_service,
303
514
  "storage_config": storage_config,
304
515
  }
305
- response = requests.post(
516
+ response = self._session.post(
306
517
  f"{self._endpoint}/source/search",
307
518
  json=req,
308
519
  headers={"Authorization": f"Bearer {self._api_key}"},
@@ -328,7 +539,7 @@ class IgniServer:
328
539
  "storage_service": storage_service,
329
540
  "storage_config": storage_config,
330
541
  }
331
- response = requests.post(
542
+ response = self._session.post(
332
543
  f"{self._endpoint}/source",
333
544
  json=req,
334
545
  headers={"Authorization": f"Bearer {self._api_key}"},
@@ -354,7 +565,7 @@ class IgniServer:
354
565
 
355
566
  def get_spec(self, id: str) -> IgniSpec:
356
567
  assert type(id) is str
357
- response = requests.get(
568
+ response = self._session.get(
358
569
  f"{self._endpoint}/spec/{id}",
359
570
  headers={"Authorization": f"Bearer {self._api_key}"},
360
571
  )
@@ -364,7 +575,7 @@ class IgniServer:
364
575
  return IgniSpec(response["id"], response)
365
576
 
366
577
  def list_specs(self) -> list[str]:
367
- response = requests.get(
578
+ response = self._session.get(
368
579
  f"{self._endpoint}/spec",
369
580
  headers={"Authorization": f"Bearer {self._api_key}"},
370
581
  )
@@ -382,6 +593,7 @@ class IgniServer:
382
593
  frame_rate,
383
594
  ready_hook=None,
384
595
  steer_hook=None,
596
+ ttl=None,
385
597
  ) -> IgniSpec:
386
598
  assert type(width) is int
387
599
  assert type(height) is int
@@ -390,6 +602,7 @@ class IgniServer:
390
602
  assert type(frame_rate) is Fraction
391
603
  assert type(ready_hook) is str or ready_hook is None
392
604
  assert type(steer_hook) is str or steer_hook is None
605
+ assert ttl is None or type(ttl) is int
393
606
 
394
607
  req = {
395
608
  "width": width,
@@ -402,8 +615,9 @@ class IgniServer:
402
615
  "frame_rate": [frame_rate.numerator, frame_rate.denominator],
403
616
  "ready_hook": ready_hook,
404
617
  "steer_hook": steer_hook,
618
+ "ttl": ttl,
405
619
  }
406
- response = requests.post(
620
+ response = self._session.post(
407
621
  f"{self._endpoint}/spec",
408
622
  json=req,
409
623
  headers={"Authorization": f"Bearer {self._api_key}"},
@@ -416,7 +630,7 @@ class IgniServer:
416
630
 
417
631
  def delete_spec(self, id: str):
418
632
  assert type(id) is str
419
- response = requests.delete(
633
+ response = self._session.delete(
420
634
  f"{self._endpoint}/spec/{id}",
421
635
  headers={"Authorization": f"Bearer {self._api_key}"},
422
636
  )
@@ -453,7 +667,7 @@ class IgniServer:
453
667
  "frames": req_frames,
454
668
  "terminal": terminal,
455
669
  }
456
- response = requests.post(
670
+ response = self._session.post(
457
671
  f"{self._endpoint}/spec/{spec_id}/part",
458
672
  json=req,
459
673
  headers={"Authorization": f"Bearer {self._api_key}"},
@@ -463,6 +677,84 @@ class IgniServer:
463
677
  response = response.json()
464
678
  assert response["status"] == "ok"
465
679
 
680
+ def push_spec_part_block(
681
+ self, spec_id: str, pos, blocks, terminal, compression="gzip"
682
+ ):
683
+ if type(spec_id) is IgniSpec:
684
+ spec_id = spec_id._id
685
+ assert type(spec_id) is str
686
+ assert type(pos) is int
687
+ assert type(blocks) is list
688
+ assert type(terminal) is bool
689
+ assert compression is None or compression == "gzip"
690
+
691
+ req_blocks = []
692
+ for block in blocks:
693
+ assert type(block) is _FrameExpressionBlock
694
+ block_body = block.as_dict()
695
+ block_frames = len(block_body["frame_exprs"])
696
+ block_body = json.dumps(block_body).encode("utf-8")
697
+ if compression == "gzip":
698
+ block_body = gzip.compress(block_body, 1)
699
+ block_body = base64.b64encode(block_body).decode("utf-8")
700
+ req_blocks.append(
701
+ {
702
+ "frames": block_frames,
703
+ "compression": compression,
704
+ "body": block_body,
705
+ }
706
+ )
707
+
708
+ req = {
709
+ "pos": pos,
710
+ "terminal": terminal,
711
+ "blocks": req_blocks,
712
+ }
713
+ response = self._session.post(
714
+ f"{self._endpoint}/spec/{spec_id}/part_block",
715
+ json=req,
716
+ headers={"Authorization": f"Bearer {self._api_key}"},
717
+ )
718
+ if not response.ok:
719
+ raise Exception(response.text)
720
+ response = response.json()
721
+ assert response["status"] == "ok"
722
+
723
+ def frame(self, width, height, pix_fmt, frame_expr, compression="gzip"):
724
+ assert type(frame_expr) is FilterExpr or type(frame_expr) is SourceExpr
725
+ assert compression is None or compression in ["gzip"]
726
+ feb = _FrameExpressionBlock()
727
+ feb.insert_frame(frame_expr)
728
+ feb_body = feb.as_dict()
729
+
730
+ feb_body = json.dumps(feb_body).encode("utf-8")
731
+ if compression == "gzip":
732
+ feb_body = gzip.compress(feb_body, 1)
733
+ feb_body = base64.b64encode(feb_body).decode("utf-8")
734
+ req = {
735
+ "width": width,
736
+ "height": height,
737
+ "pix_fmt": pix_fmt,
738
+ "compression": compression,
739
+ "block": {
740
+ "frames": 1,
741
+ "compression": compression,
742
+ "body": feb_body,
743
+ },
744
+ }
745
+ response = self._session.post(
746
+ f"{self._endpoint}/frame",
747
+ json=req,
748
+ headers={"Authorization": f"Bearer {self._api_key}"},
749
+ )
750
+ if not response.ok:
751
+ raise Exception(response.text)
752
+ response_body = response.content
753
+ assert type(response_body) is bytes
754
+ if compression == "gzip":
755
+ response_body = gzip.decompress(response_body)
756
+ return response_body
757
+
466
758
 
467
759
  class YrdenSpec:
468
760
  """
@@ -543,12 +835,11 @@ class YrdenSpec:
543
835
  }
544
836
  for k, v in filters.items()
545
837
  }
546
- arrays = []
547
838
 
548
839
  if verbose:
549
840
  print(f"Sending to server. Spec is {len(spec_obj_json_gzip_b64)} bytes")
550
841
 
551
- resp = server._new(spec_obj_json_gzip_b64, sources, filters, arrays, self._fmt)
842
+ resp = server._new(spec_obj_json_gzip_b64, sources, filters, self._fmt)
552
843
  hls_video_url = resp["stream_url"]
553
844
  hls_player_url = resp["player_url"]
554
845
  namespace = resp["namespace"]
@@ -616,9 +907,7 @@ class YrdenSpec:
616
907
  }
617
908
  for k, v in filters.items()
618
909
  }
619
- arrays = []
620
-
621
- resp = server._new(spec_obj_json_gzip_b64, sources, filters, arrays, self._fmt)
910
+ resp = server._new(spec_obj_json_gzip_b64, sources, filters, self._fmt)
622
911
  namespace = resp["namespace"]
623
912
  return YrdenLoader(server, namespace, self._domain)
624
913
 
@@ -652,14 +941,12 @@ class YrdenSpec:
652
941
  }
653
942
  for k, v in filters.items()
654
943
  }
655
- arrays = []
656
944
 
657
945
  resp = server._export(
658
946
  pth,
659
947
  spec_obj_json_gzip_b64,
660
948
  sources,
661
949
  filters,
662
- arrays,
663
950
  self._fmt,
664
951
  encoder,
665
952
  encoder_opts,
@@ -692,12 +979,11 @@ class YrdenSpec:
692
979
  }
693
980
  for k, v in filters.items()
694
981
  }
695
- arrays = []
696
982
  end_t = time.time()
697
983
  out["vrod_create_spec"] = end_t - start_t
698
984
 
699
985
  start = time.time()
700
- resp = server._new(pth, sources, filters, arrays, self._fmt)
986
+ resp = server._new(pth, sources, filters, self._fmt)
701
987
  end = time.time()
702
988
  out["vrod_register"] = end - start
703
989
 
@@ -735,12 +1021,11 @@ class YrdenSpec:
735
1021
  }
736
1022
  for k, v in filters.items()
737
1023
  }
738
- arrays = []
739
1024
  end_t = time.time()
740
1025
  out["dve2_create_spec"] = end_t - start_t
741
1026
 
742
1027
  start = time.time()
743
- resp = server._export(pth, sources, filters, arrays, self._fmt, None, None)
1028
+ resp = server._export(pth, sources, filters, self._fmt, None, None)
744
1029
  resp.raise_for_status()
745
1030
  end = time.time()
746
1031
  out["dve2_exec"] = end - start
@@ -861,12 +1146,11 @@ class YrdenServer:
861
1146
  resp["ts"] = [Fraction(x[0], x[1]) for x in resp["ts"]]
862
1147
  return resp
863
1148
 
864
- def _new(self, spec, sources, filters, arrays, fmt):
1149
+ def _new(self, spec, sources, filters, fmt):
865
1150
  req = {
866
1151
  "spec": spec,
867
1152
  "sources": sources,
868
1153
  "filters": filters,
869
- "arrays": arrays,
870
1154
  "width": fmt["width"],
871
1155
  "height": fmt["height"],
872
1156
  "pix_fmt": fmt["pix_fmt"],
@@ -878,14 +1162,11 @@ class YrdenServer:
878
1162
 
879
1163
  return r.json()
880
1164
 
881
- def _export(
882
- self, pth, spec, sources, filters, arrays, fmt, encoder, encoder_opts, format
883
- ):
1165
+ def _export(self, pth, spec, sources, filters, fmt, encoder, encoder_opts, format):
884
1166
  req = {
885
1167
  "spec": spec,
886
1168
  "sources": sources,
887
1169
  "filters": filters,
888
- "arrays": arrays,
889
1170
  "width": fmt["width"],
890
1171
  "height": fmt["height"],
891
1172
  "pix_fmt": fmt["pix_fmt"],
@@ -915,7 +1196,12 @@ class YrdenServer:
915
1196
 
916
1197
  def __del__(self):
917
1198
  if self._proc is not None:
918
- self._proc.kill()
1199
+ self._proc.terminate()
1200
+ try:
1201
+ self._proc.wait(timeout=1)
1202
+ except subprocess.TimeoutExpired:
1203
+ self._proc.kill()
1204
+ self._proc.wait()
919
1205
 
920
1206
 
921
1207
  class YrdenSource:
vidformer/cv2/__init__.py CHANGED
@@ -18,13 +18,13 @@ try:
18
18
  except Exception:
19
19
  _opencv2 = None
20
20
 
21
- import numpy as np
22
-
21
+ import re
23
22
  import uuid
24
- from fractions import Fraction
25
- from bisect import bisect_right
26
23
  import zlib
27
- import re
24
+ from bisect import bisect_right
25
+ from fractions import Fraction
26
+
27
+ import numpy as np
28
28
 
29
29
  CAP_PROP_POS_MSEC = 0
30
30
  CAP_PROP_POS_FRAMES = 1
@@ -51,6 +51,7 @@ LINE_AA = 16
51
51
  _inline_mat = vf.Filter("_inline_mat")
52
52
  _slice_mat = vf.Filter("_slice_mat")
53
53
  _slice_write_mat = vf.Filter("_slice_write_mat")
54
+ _black = vf.Filter("_black")
54
55
 
55
56
 
56
57
  _filter_scale = vf.Filter("Scale")
@@ -61,6 +62,7 @@ _filter_line = vf.Filter("cv2.line")
61
62
  _filter_circle = vf.Filter("cv2.circle")
62
63
  _filter_addWeighted = vf.Filter("cv2.addWeighted")
63
64
  _filter_ellipse = vf.Filter("cv2.ellipse")
65
+ _set_to = vf.Filter("cv2.setTo")
64
66
 
65
67
 
66
68
  def _ts_to_fps(timestamps):
@@ -89,11 +91,30 @@ def set_server(server):
89
91
  _global_cv2_server = server
90
92
 
91
93
 
94
+ _PIX_FMT_MAP = {
95
+ "rgb24": "rgb24",
96
+ "yuv420p": "rgb24",
97
+ "yuv422p": "rgb24",
98
+ "yuv444p": "rgb24",
99
+ "yuvj420p": "rgb24",
100
+ "yuvj422p": "rgb24",
101
+ "yuvj444p": "rgb24",
102
+ "gray": "gray",
103
+ }
104
+
105
+
106
+ def _top_level_pix_fmt(pix_fmt):
107
+ if pix_fmt in _PIX_FMT_MAP:
108
+ return _PIX_FMT_MAP[pix_fmt]
109
+ raise Exception(f"Unsupported pix_fmt {pix_fmt}")
110
+
111
+
92
112
  class Frame:
93
113
  def __init__(self, f, fmt):
94
114
  self._f = f
95
115
  self._fmt = fmt
96
- self.shape = (fmt["height"], fmt["width"], 3)
116
+ channels = 3 if _top_level_pix_fmt(fmt["pix_fmt"]) == "rgb24" else 1
117
+ self.shape = (fmt["height"], fmt["width"], channels)
97
118
 
98
119
  # denotes that the frame has not yet been modified
99
120
  # when a frame is modified, it is converted to rgb24 first
@@ -101,13 +122,22 @@ class Frame:
101
122
 
102
123
  def _mut(self):
103
124
  if self._modified:
104
- assert self._fmt["pix_fmt"] == "rgb24"
125
+ assert self._fmt["pix_fmt"] in ["rgb24", "gray"]
105
126
  return
106
127
 
107
128
  self._modified = True
108
- if self._fmt["pix_fmt"] != "rgb24":
129
+ if (
130
+ self._fmt["pix_fmt"] != "rgb24"
131
+ and _top_level_pix_fmt(self._fmt["pix_fmt"]) == "rgb24"
132
+ ):
109
133
  self._f = _filter_scale(self._f, pix_fmt="rgb24")
110
134
  self._fmt["pix_fmt"] = "rgb24"
135
+ elif (
136
+ self._fmt["pix_fmt"] != "gray"
137
+ and _top_level_pix_fmt(self._fmt["pix_fmt"]) == "gray"
138
+ ):
139
+ self._f = _filter_scale(self._f, pix_fmt="gray")
140
+ self._fmt["pix_fmt"] = "gray"
111
141
 
112
142
  def copy(self):
113
143
  return Frame(self._f, self._fmt.copy())
@@ -118,16 +148,29 @@ class Frame:
118
148
  """
119
149
 
120
150
  self._mut()
121
- spec = vf.YrdenSpec([Fraction(0, 1)], lambda t, i: self._f, self._fmt)
122
- loader = spec.load(_server())
123
-
124
- frame_raster_rgb24 = loader[0]
125
- assert type(frame_raster_rgb24) is bytes
126
- assert len(frame_raster_rgb24) == self.shape[0] * self.shape[1] * 3
127
- raw_data_array = np.frombuffer(frame_raster_rgb24, dtype=np.uint8)
128
- frame = raw_data_array.reshape(self.shape)
129
- frame = frame[:, :, ::-1] # convert RGB to BGR
130
- return frame
151
+ server = _server()
152
+ if type(server) is vf.YrdenServer:
153
+ spec = vf.YrdenSpec([Fraction(0, 1)], lambda t, i: self._f, self._fmt)
154
+ loader = spec.load(_server())
155
+
156
+ frame_raster_rgb24 = loader[0]
157
+ assert type(frame_raster_rgb24) is bytes
158
+ assert len(frame_raster_rgb24) == self.shape[0] * self.shape[1] * 3
159
+ raw_data_array = np.frombuffer(frame_raster_rgb24, dtype=np.uint8)
160
+ frame = raw_data_array.reshape(self.shape)
161
+ frame = frame[:, :, ::-1] # convert RGB to BGR
162
+ return frame
163
+ else:
164
+ frame = server.frame(
165
+ self.shape[1], self.shape[0], self._fmt["pix_fmt"], self._f
166
+ )
167
+ assert type(frame) is bytes
168
+ assert len(frame) == self.shape[0] * self.shape[1] * self.shape[2]
169
+ raw_data_array = np.frombuffer(frame, dtype=np.uint8)
170
+ frame = raw_data_array.reshape(self.shape)
171
+ if self.shape[2] == 3:
172
+ frame = frame[:, :, ::-1] # convert RGB to BGR
173
+ return frame
131
174
 
132
175
  def __getitem__(self, key):
133
176
  if not isinstance(key, tuple):
@@ -171,49 +214,75 @@ class Frame:
171
214
  return Frame(f, fmt)
172
215
 
173
216
  def __setitem__(self, key, value):
174
- value = frameify(value, "value")
175
-
176
- if not isinstance(key, tuple):
177
- raise NotImplementedError("Only 2D slicing is supported")
178
-
179
- if len(key) != 2:
180
- raise NotImplementedError("Only 2D slicing is supported")
181
-
182
- if not all(isinstance(x, slice) for x in key):
183
- raise NotImplementedError("Only 2D slicing is supported")
184
-
185
- miny = key[0].start if key[0].start is not None else 0
186
- maxy = key[0].stop if key[0].stop is not None else self.shape[0]
187
- minx = key[1].start if key[1].start is not None else 0
188
- maxx = key[1].stop if key[1].stop is not None else self.shape[1]
189
-
190
- # handle negative indices
191
- if miny < 0:
192
- miny = self.shape[0] + miny
193
- if maxy < 0:
194
- maxy = self.shape[0] + maxy
195
- if minx < 0:
196
- minx = self.shape[1] + minx
197
- if maxx < 0:
198
- maxx = self.shape[1] + maxx
199
-
200
- if (
201
- maxy <= miny
202
- or maxx <= minx
203
- or miny < 0
204
- or minx < 0
205
- or maxy > self.shape[0]
206
- or maxx > self.shape[1]
207
- ):
208
- raise NotImplementedError("Invalid slice")
209
-
210
- if value.shape[0] != maxy - miny or value.shape[1] != maxx - minx:
211
- raise NotImplementedError("Shape mismatch")
217
+ if type(key) is tuple:
218
+ value = frameify(value, "value")
219
+
220
+ if len(key) != 2:
221
+ raise NotImplementedError("Only 2D slicing is supported")
222
+
223
+ if not all(isinstance(x, slice) for x in key):
224
+ raise NotImplementedError("Only 2D slicing is supported")
225
+
226
+ miny = key[0].start if key[0].start is not None else 0
227
+ maxy = key[0].stop if key[0].stop is not None else self.shape[0]
228
+ minx = key[1].start if key[1].start is not None else 0
229
+ maxx = key[1].stop if key[1].stop is not None else self.shape[1]
230
+
231
+ # handle negative indices
232
+ if miny < 0:
233
+ miny = self.shape[0] + miny
234
+ if maxy < 0:
235
+ maxy = self.shape[0] + maxy
236
+ if minx < 0:
237
+ minx = self.shape[1] + minx
238
+ if maxx < 0:
239
+ maxx = self.shape[1] + maxx
240
+
241
+ if (
242
+ maxy <= miny
243
+ or maxx <= minx
244
+ or miny < 0
245
+ or minx < 0
246
+ or maxy > self.shape[0]
247
+ or maxx > self.shape[1]
248
+ ):
249
+ raise NotImplementedError("Invalid slice")
250
+
251
+ if value.shape[0] != maxy - miny or value.shape[1] != maxx - minx:
252
+ raise NotImplementedError("Shape mismatch")
253
+
254
+ self._mut()
255
+ value._mut()
256
+
257
+ self._f = _slice_write_mat(self._f, value._f, miny, maxy, minx, maxx)
258
+ elif type(key) is Frame or type(key) is np.ndarray:
259
+ key = frameify(key, "key")
260
+
261
+ if key.shape[0] != self.shape[0] or key.shape[1] != self.shape[1]:
262
+ raise NotImplementedError("Shape mismatch")
263
+
264
+ if key.shape[2] != 1:
265
+ raise NotImplementedError("Only 1-channel mask frames are supported")
266
+
267
+ # Value should be a bgr or bgra color
268
+ if (type(value) is not list and type(value) is not tuple) or len(
269
+ value
270
+ ) not in [3, 4]:
271
+ raise NotImplementedError(
272
+ "Value should be a 3 or 4 element list or tuple"
273
+ )
274
+ value = [float(x) for x in value]
275
+ if len(value) == 3:
276
+ value.append(255.0)
212
277
 
213
- self._mut()
214
- value._mut()
278
+ self._mut()
279
+ key._mut()
215
280
 
216
- self._f = _slice_write_mat(self._f, value._f, miny, maxy, minx, maxx)
281
+ self._f = _set_to(self._f, value, key._f)
282
+ else:
283
+ raise NotImplementedError(
284
+ "__setitem__ only supports slicing by a 2d tuple or a mask frame"
285
+ )
217
286
 
218
287
 
219
288
  def _inline_frame(arr):
@@ -283,7 +352,7 @@ class VideoCapture:
283
352
  elif prop == CAP_PROP_FRAME_HEIGHT:
284
353
  return self._source.fmt()["height"]
285
354
  elif prop == CAP_PROP_FRAME_COUNT:
286
- return len(self._source.ts())
355
+ return len(self._source)
287
356
  elif prop == CAP_PROP_POS_FRAMES:
288
357
  return self._next_frame_idx
289
358
 
@@ -309,6 +378,20 @@ class VideoCapture:
309
378
  frame = Frame(frame, self._source.fmt())
310
379
  return True, frame
311
380
 
381
+ def __getitem__(self, key):
382
+ if not isinstance(key, int):
383
+ raise NotImplementedError("Only integer indexing is supported")
384
+ if key < 0:
385
+ key = len(self._source) + key
386
+ if key < 0 or key >= len(self._source):
387
+ raise IndexError("Index out of bounds")
388
+ frame = self._source.iloc[key]
389
+ frame = Frame(frame, self._source.fmt())
390
+ return frame
391
+
392
+ def __len__(self):
393
+ return len(self._source)
394
+
312
395
  def release(self):
313
396
  pass
314
397
 
@@ -341,6 +424,8 @@ class _IgniVideoWriter:
341
424
  fps,
342
425
  size,
343
426
  batch_size=1024,
427
+ compression="gzip",
428
+ ttl=3600,
344
429
  vod_segment_length=Fraction(2, 1),
345
430
  ):
346
431
  server = _server()
@@ -359,26 +444,34 @@ class _IgniVideoWriter:
359
444
  assert isinstance(size, tuple) or isinstance(size, list)
360
445
  assert len(size) == 2
361
446
  width, height = size
447
+ assert ttl is None or isinstance(ttl, int)
362
448
  self._spec = server.create_spec(
363
- width, height, "yuv420p", vod_segment_length, 1 / self._f_time
449
+ width, height, "yuv420p", vod_segment_length, 1 / self._f_time, ttl=ttl
364
450
  )
365
451
  self._batch_size = batch_size
452
+ assert compression is None or compression in ["gzip"]
453
+ self._compression = compression
366
454
  self._idx = 0
367
- self._frame_buffer = []
455
+ self._feb = vf._FrameExpressionBlock()
368
456
 
369
457
  def _flush(self, terminal=False):
370
458
  server = _server()
371
- server.push_spec_part(
372
- self._spec,
373
- self._idx - len(self._frame_buffer),
374
- self._frame_buffer,
375
- terminal=terminal,
376
- )
377
- self._frame_buffer = []
378
-
379
- def _explicit_terminate(self):
380
- server = _server()
381
- server.push_spec_part(self._spec._id, self._idx, [], terminal=True)
459
+ if len(self._feb) > 0:
460
+ server.push_spec_part_block(
461
+ self._spec,
462
+ self._idx - len(self._feb),
463
+ [self._feb],
464
+ terminal=terminal,
465
+ compression=self._compression,
466
+ )
467
+ self._feb = vf._FrameExpressionBlock()
468
+ else:
469
+ server.push_spec_part_block(
470
+ self._spec,
471
+ self._idx - len(self._feb),
472
+ [],
473
+ terminal=terminal,
474
+ )
382
475
 
383
476
  def spec(self):
384
477
  return self._spec
@@ -397,18 +490,14 @@ class _IgniVideoWriter:
397
490
  if frame._fmt["pix_fmt"] != self._spec._fmt["pix_fmt"]:
398
491
  f_obj = _filter_scale(frame._f, pix_fmt=self._spec._fmt["pix_fmt"])
399
492
  frame = Frame(f_obj, self._spec._fmt)
400
- t = self._f_time * self._idx
401
- self._frame_buffer.append((t, frame._f if frame is not None else None))
493
+ self._feb.insert_frame(frame._f if frame is not None else None)
402
494
  self._idx += 1
403
495
 
404
- if len(self._frame_buffer) >= self._batch_size:
496
+ if len(self._feb) >= self._batch_size:
405
497
  self._flush()
406
498
 
407
499
  def release(self):
408
- if len(self._frame_buffer) > 0:
409
- self._flush(True)
410
- else:
411
- self._explicit_terminate()
500
+ self._flush(True)
412
501
 
413
502
 
414
503
  class _YrdenVideoWriter:
@@ -478,18 +567,33 @@ def frameify(obj, field_name=None):
478
567
  def imread(path, *args):
479
568
  if len(args) > 0:
480
569
  raise NotImplementedError("imread does not support additional arguments")
481
-
482
570
  assert path.lower().endswith((".jpg", ".jpeg", ".png"))
483
571
  server = _server()
484
- source = vf.YrdenSource(server, str(uuid.uuid4()), path, 0)
485
- frame = Frame(source.iloc[0], source.fmt())
486
- return frame
572
+
573
+ if type(server) is vf.YrdenServer:
574
+ source = vf.YrdenSource(server, str(uuid.uuid4()), path, 0)
575
+ frame = Frame(source.iloc[0], source.fmt())
576
+ return frame
577
+ else:
578
+ cap = VideoCapture(path)
579
+ assert cap.isOpened()
580
+ assert len(cap._source) == 1
581
+ ret, frame = cap.read()
582
+ assert ret
583
+ cap.release()
584
+ return frame
487
585
 
488
586
 
489
587
  def imwrite(path, img, *args):
490
588
  if len(args) > 0:
491
589
  raise NotImplementedError("imwrite does not support additional arguments")
492
590
 
591
+ server = _server()
592
+ if type(server) is vf.IgniServer:
593
+ raise NotImplementedError(
594
+ "imwrite is only supported with YrdenServer, not IgniServer"
595
+ )
596
+
493
597
  img = frameify(img)
494
598
 
495
599
  fmt = img._fmt.copy()
@@ -518,7 +622,7 @@ def imwrite(path, img, *args):
518
622
  fmt["pix_fmt"] = "yuvj420p"
519
623
 
520
624
  spec = vf.YrdenSpec(domain, lambda t, i: f, fmt)
521
- spec.save(_server(), path, encoder="mjpeg")
625
+ spec.save(server, path, encoder="mjpeg")
522
626
  else:
523
627
  raise Exception("Unsupported image format")
524
628
 
@@ -546,6 +650,39 @@ def vidplay(video, *args, **kwargs):
546
650
  raise Exception("Unsupported video type to vidplay")
547
651
 
548
652
 
653
+ def zeros(shape, dtype=np.uint8):
654
+ """
655
+ Create a black frame. Mimics numpy.zeros.
656
+ """
657
+ assert isinstance(shape, tuple) or isinstance(shape, list)
658
+ assert len(shape) == 3
659
+ assert shape[2] in [1, 3]
660
+ assert dtype == np.uint8
661
+
662
+ height, width, channels = shape
663
+ if channels == 1:
664
+ pix_fmt = "gray"
665
+ else:
666
+ pix_fmt = "rgb24"
667
+
668
+ f = _black(width=width, height=height, pix_fmt=pix_fmt)
669
+ fmt = {"width": width, "height": height, "pix_fmt": pix_fmt}
670
+ return Frame(f, fmt)
671
+
672
+
673
+ def resize(src, dsize):
674
+ src = frameify(src)
675
+ src._mut()
676
+
677
+ assert isinstance(dsize, tuple) or isinstance(dsize, list)
678
+ assert len(dsize) == 2
679
+ width, height = dsize
680
+
681
+ f = _filter_scale(src._f, width=width, height=height)
682
+ fmt = {"width": width, "height": height, "pix_fmt": src._fmt["pix_fmt"]}
683
+ return Frame(f, fmt)
684
+
685
+
549
686
  def rectangle(img, pt1, pt2, color, thickness=None, lineType=None, shift=None):
550
687
  """
551
688
  cv.rectangle( img, pt1, pt2, color[, thickness[, lineType[, shift]]] )
@@ -2,17 +2,23 @@
2
2
  vidformer.supervision is the [supervision](https://supervision.roboflow.com/) frontend for [vidformer](https://github.com/ixlab/vidformer).
3
3
  """
4
4
 
5
- import vidformer.cv2 as vf_cv2
5
+ from math import sqrt
6
6
 
7
- import supervision as _sv
8
7
  import numpy as np
9
- from supervision import Color, ColorPalette, ColorLookup, Detections
8
+ import supervision as _sv
9
+ from supervision import Color, ColorLookup, ColorPalette, Detections
10
10
  from supervision.annotators.utils import resolve_color, resolve_text_background_xyxy
11
- from supervision.detection.utils import spread_out_boxes
12
11
  from supervision.config import CLASS_NAME_DATA_FIELD
13
- from math import sqrt
12
+ from supervision.detection.utils import spread_out_boxes
14
13
  from supervision.geometry.core import Position
15
14
 
15
+ import vidformer.cv2 as vf_cv2
16
+
17
+ try:
18
+ import cv2 as ocv_cv2
19
+ except ImportError:
20
+ ocv_cv2 = None
21
+
16
22
  CV2_FONT = vf_cv2.FONT_HERSHEY_SIMPLEX
17
23
 
18
24
 
@@ -271,7 +277,6 @@ class DotAnnotator:
271
277
  outline_thickness: int = 0,
272
278
  outline_color=Color.BLACK,
273
279
  ):
274
-
275
280
  self.color = color
276
281
  self.radius: int = radius
277
282
  self.position: Position = position
@@ -536,3 +541,89 @@ class LabelAnnotator:
536
541
  thickness=-1,
537
542
  )
538
543
  return scene
544
+
545
+
546
+ class MaskAnnotator:
547
+ def __init__(
548
+ self,
549
+ color=ColorPalette.DEFAULT,
550
+ opacity: float = 0.5,
551
+ color_lookup: ColorLookup = ColorLookup.CLASS,
552
+ ):
553
+ self.color = color
554
+ self.opacity = opacity
555
+ self.color_lookup: ColorLookup = color_lookup
556
+
557
+ def annotate(
558
+ self,
559
+ scene,
560
+ detections: Detections,
561
+ custom_color_lookup=None,
562
+ ):
563
+ if detections.mask is None:
564
+ return scene
565
+
566
+ colored_mask = scene.copy()
567
+
568
+ for detection_idx in np.flip(np.argsort(detections.box_area)):
569
+ color = resolve_color(
570
+ color=self.color,
571
+ detections=detections,
572
+ detection_idx=detection_idx,
573
+ color_lookup=(
574
+ self.color_lookup
575
+ if custom_color_lookup is None
576
+ else custom_color_lookup
577
+ ),
578
+ )
579
+ mask = detections.mask[detection_idx]
580
+ colored_mask[mask] = color.as_bgr()
581
+
582
+ vf_cv2.addWeighted(
583
+ colored_mask, self.opacity, scene, 1 - self.opacity, 0, dst=scene
584
+ )
585
+ return scene
586
+
587
+
588
+ class MaskStreamWriter:
589
+ def __init__(self, path: str, shape: tuple):
590
+ # Shape should be (width, height)
591
+ assert ocv_cv2 is not None, "OpenCV cv2 is required for ExternDetectionsBuilder"
592
+ assert type(shape) is tuple, "shape must be a tuple"
593
+ assert len(shape) == 2, "shape must be a tuple of length 2"
594
+ self._shape = (shape[1], shape[0])
595
+ self._writer = ocv_cv2.VideoWriter(
596
+ path, ocv_cv2.VideoWriter_fourcc(*"FFV1"), 1, shape, isColor=False
597
+ )
598
+ assert self._writer.isOpened(), f"Failed to open video writer at {path}"
599
+ self._i = 0
600
+
601
+ def write_detections(self, detections: Detections):
602
+ if len(detections) == 0:
603
+ return self._i
604
+
605
+ mask = detections.mask
606
+ assert (
607
+ mask.shape[1:] == self._shape
608
+ ), f"mask shape ({mask.shape[:1]}) must match the shape of the video ({self._shape})"
609
+ for i in range(mask.shape[0]):
610
+ frame_uint8 = detections.mask[i].astype(np.uint8)
611
+ self._writer.write(frame_uint8)
612
+ self._i += 1
613
+ return self._i
614
+
615
+ def release(self):
616
+ self._writer.release()
617
+
618
+
619
+ def populate_mask(
620
+ detections: Detections, mask_stream: vf_cv2.VideoCapture, frame_idx: int
621
+ ):
622
+ assert type(detections) is Detections
623
+ assert detections.mask is None
624
+ detections.mask = []
625
+ assert len(detections) + frame_idx <= len(mask_stream)
626
+ for i in range(len(detections)):
627
+ mask = mask_stream[frame_idx + i]
628
+ assert mask.shape[2] == 1, "mask must be a single channel image"
629
+ detections.mask.append(mask)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: vidformer
3
- Version: 0.10.1
3
+ Version: 0.12.0
4
4
  Summary: vidformer-py is a Python 🐍 interface for [vidformer](https://github.com/ixlab/vidformer).
5
5
  Author-email: Dominik Winecki <dominikwinecki@gmail.com>
6
6
  Requires-Python: >=3.8
@@ -0,0 +1,6 @@
1
+ vidformer/__init__.py,sha256=2_IA8eCF8xIWqgdcpC06CSEsX_b2DPpOww3tuQlY3rg,55692
2
+ vidformer/cv2/__init__.py,sha256=cp1qJPpxpRGCE3elmoHDxhzafZbopZ9wIkKcZJJI8HM,30105
3
+ vidformer/supervision/__init__.py,sha256=dRHAcHiZN68gUH_2m3o7Ohsv3NBGxF4XGPeI0pn2_K4,20346
4
+ vidformer-0.12.0.dist-info/WHEEL,sha256=_2ozNFCLWc93bK4WKHCO-eDUENDlo-dgc9cU3qokYO4,82
5
+ vidformer-0.12.0.dist-info/METADATA,sha256=C3OsKiJjYPCgiUblJUan2-aQG5TOprdCn2cduqJBow0,1800
6
+ vidformer-0.12.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: flit 3.10.1
2
+ Generator: flit 3.11.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,6 +0,0 @@
1
- vidformer/__init__.py,sha256=7ZUQSCEoTkxGFORCWhL1WAgS_ii0Xu_kaipjjstUAn4,44916
2
- vidformer/cv2/__init__.py,sha256=DGm5NB4FGCHxPVez-yO748DjocKruxn4QBqqThgskWI,25555
3
- vidformer/supervision/__init__.py,sha256=unJMfbabIBQ36iftcs6QUM3mzdWFRxrlLdk_Z1F1oO8,17489
4
- vidformer-0.10.1.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
5
- vidformer-0.10.1.dist-info/METADATA,sha256=fzOpw2PCiHhZgeFS6rBxrlutcMs7v6oznjnWz-f6j-Y,1800
6
- vidformer-0.10.1.dist-info/RECORD,,