lightning-pose 2.4.2__tar.gz → 2.4.3__tar.gz
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.
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/PKG-INFO +1 -1
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/api/model.py +28 -2
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/cli/commands/predict.py +18 -2
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/data/bboxes.py +111 -3
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/data/video/__init__.py +6 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/data/video/dali.py +85 -107
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/data/video/factory.py +6 -3
- lightning_pose-2.4.3/lightning_pose/data/video/opencv.py +203 -0
- lightning_pose-2.4.3/lightning_pose/data/video/pynvvc.py +298 -0
- lightning_pose-2.4.3/lightning_pose/data/video/utils.py +339 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/backbones/vit_dino.py +3 -1
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/heads/heatmap.py +5 -1
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/utils/cropzoom.py +4 -6
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/utils/io.py +19 -1
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/utils/predictions.py +51 -13
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/pyproject.toml +1 -1
- lightning_pose-2.4.2/lightning_pose/data/video/opencv.py +0 -422
- lightning_pose-2.4.2/lightning_pose/data/video/pynvvc.py +0 -489
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/LICENSE +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/README.md +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/__init__.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/api/__init__.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/api/model_config.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/api/model_runtime.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/callbacks.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/cli/__init__.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/cli/commands/__init__.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/cli/commands/convert.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/cli/commands/create_bbox.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/cli/commands/crop.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/cli/commands/export.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/cli/commands/recommend.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/cli/commands/remap.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/cli/commands/run_app.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/cli/commands/smooth_bbox.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/cli/commands/train.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/cli/friendly.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/cli/main.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/cli/types.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/converters/__init__.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/converters/dlc.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/converters/sleap.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/data/__init__.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/data/augmentations.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/data/cameras.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/data/datamodules.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/data/datasets.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/data/datatypes.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/data/extractor.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/data/factory.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/data/heatmaps.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/data/utils.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/losses/__init__.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/losses/factory.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/losses/losses.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/metrics.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/migrations/__init__.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/migrations/migrations.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/migrations/rename_time_directories.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/__init__.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/backbones/__init__.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/backbones/factory.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/backbones/vit.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/backbones/vit_sam.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/backbones/vit_sam2.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/base.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/datatypes.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/factory.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/heads/__init__.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/heads/heatmap_mhcrnn.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/heads/regression.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/heatmap_tracker.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/heatmap_tracker_mhcrnn.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/heatmap_tracker_multiview.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/models/regression_tracker.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/train.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/utils/__init__.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/utils/device.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/utils/inference_types.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/utils/pca.py +0 -0
- {lightning_pose-2.4.2 → lightning_pose-2.4.3}/lightning_pose/utils/recommender.py +0 -0
|
@@ -728,7 +728,8 @@ class Model(_RuntimeMixin): # pyright: ignore[reportGeneralTypeIssues]
|
|
|
728
728
|
bbox_file (str | Path, optional): Path to a per-frame bbox CSV (columns x, y, h, w;
|
|
729
729
|
one row per frame). When provided, each frame is cropped to its bounding box
|
|
730
730
|
before being passed to the model, and predictions are returned in the original
|
|
731
|
-
coordinate space. Single-view only
|
|
731
|
+
coordinate space. Single-view only; for multiview models use
|
|
732
|
+
``predict_on_video_file_multiview(bbox_file_per_view=...)``. Defaults to None.
|
|
732
733
|
|
|
733
734
|
Returns:
|
|
734
735
|
PredictionResult: A PredictionResult object containing the predictions and metrics.
|
|
@@ -798,6 +799,7 @@ class Model(_RuntimeMixin): # pyright: ignore[reportGeneralTypeIssues]
|
|
|
798
799
|
generate_labeled_video: bool = False,
|
|
799
800
|
progress_file: Path | None = None,
|
|
800
801
|
reader: _Reader | None = None,
|
|
802
|
+
bbox_file_per_view: list[str] | list[Path] | None = None,
|
|
801
803
|
) -> MultiviewPredictionResult:
|
|
802
804
|
"""Version of ``predict_on_video_file`` that accesses multiple camera views of each frame.
|
|
803
805
|
|
|
@@ -813,10 +815,20 @@ class Model(_RuntimeMixin): # pyright: ignore[reportGeneralTypeIssues]
|
|
|
813
815
|
reader: which video-reading backend to use, "dali", "pynvvc", or "opencv". None
|
|
814
816
|
(default) auto-selects pynvvc if it's usable on this machine for this video,
|
|
815
817
|
else dali if it's installed, else opencv (the portable fallback, always
|
|
816
|
-
available).
|
|
818
|
+
available). Multiview bbox cropping currently requires (and defaults to)
|
|
819
|
+
"dali".
|
|
820
|
+
bbox_file_per_view: optional list of per-frame bbox CSVs (columns x, y, h, w; one
|
|
821
|
+
row per frame), in the same order as ``video_file_per_view``. When provided,
|
|
822
|
+
each view's frames are cropped to that view's bboxes before being passed to
|
|
823
|
+
the model, and predictions are returned in the original coordinate space.
|
|
817
824
|
|
|
818
825
|
Returns:
|
|
819
826
|
object containing the predictions and metrics for each view.
|
|
827
|
+
|
|
828
|
+
Raises:
|
|
829
|
+
ValueError: if the model is not multiview, the number of video or bbox files does
|
|
830
|
+
not match the number of views, or a bbox CSV's row count differs from its
|
|
831
|
+
video's frame count.
|
|
820
832
|
"""
|
|
821
833
|
if not self.config.is_multi_view():
|
|
822
834
|
raise ValueError('predict_on_video_file_multiview requires a multi-view model')
|
|
@@ -830,6 +842,16 @@ class Model(_RuntimeMixin): # pyright: ignore[reportGeneralTypeIssues]
|
|
|
830
842
|
)
|
|
831
843
|
|
|
832
844
|
video_file_per_view = [Path(f) for f in video_file_per_view]
|
|
845
|
+
if bbox_file_per_view is not None and len(bbox_file_per_view) != len(view_names):
|
|
846
|
+
raise ValueError(
|
|
847
|
+
f'expected {len(view_names)} bbox files (one per view), '
|
|
848
|
+
f'got {len(bbox_file_per_view)}'
|
|
849
|
+
)
|
|
850
|
+
# pair each bbox file with its video before the videos are put in view order
|
|
851
|
+
video_to_bbox_file = (
|
|
852
|
+
dict(zip(video_file_per_view, map(Path, bbox_file_per_view), strict=True))
|
|
853
|
+
if bbox_file_per_view is not None else None
|
|
854
|
+
)
|
|
833
855
|
|
|
834
856
|
if output_dir == self.__class__.UNSPECIFIED:
|
|
835
857
|
output_dir = self.video_preds_dir()
|
|
@@ -859,6 +881,10 @@ class Model(_RuntimeMixin): # pyright: ignore[reportGeneralTypeIssues]
|
|
|
859
881
|
output_pred_file=prediction_csv_file_list,
|
|
860
882
|
progress_file=progress_file,
|
|
861
883
|
reader=reader,
|
|
884
|
+
bbox_file=(
|
|
885
|
+
[str(video_to_bbox_file[v]) for v in video_file_per_view]
|
|
886
|
+
if video_to_bbox_file is not None else None
|
|
887
|
+
),
|
|
862
888
|
)
|
|
863
889
|
if generate_labeled_video:
|
|
864
890
|
for video_file, preds_df in zip(video_file_per_view, df_list, strict=True):
|
|
@@ -120,7 +120,8 @@ def register_parser(subparsers: Any) -> argparse.ArgumentParser:
|
|
|
120
120
|
"directory containing bbox CSV files produced by ``litpose create_bbox`` or a "
|
|
121
121
|
"compatible external source. For CSV inputs, looks for ``bbox.csv`` inside this "
|
|
122
122
|
"directory. For video inputs, looks for ``<video_stem>_bbox.csv`` inside this "
|
|
123
|
-
"directory
|
|
123
|
+
"directory; for multiview models, one ``<video_stem>_bbox.csv`` per view video. "
|
|
124
|
+
"When provided, each frame is cropped to its bounding box before "
|
|
124
125
|
"being passed to the model, and predictions are saved in the original coordinate "
|
|
125
126
|
"space."
|
|
126
127
|
),
|
|
@@ -235,6 +236,7 @@ def handle(args: argparse.Namespace) -> None:
|
|
|
235
236
|
_predict_multi_type_multi_view(
|
|
236
237
|
model, input_paths, args.skip_viz, not args.overwrite,
|
|
237
238
|
progress_file=args.progress_file, reader=args.reader,
|
|
239
|
+
bbox_dir=args.bbox_dir,
|
|
238
240
|
)
|
|
239
241
|
else:
|
|
240
242
|
for p in input_paths:
|
|
@@ -320,6 +322,7 @@ def _predict_multi_type_multi_view(
|
|
|
320
322
|
skip_existing: bool,
|
|
321
323
|
progress_file: Path | None = None,
|
|
322
324
|
reader: _Reader | None = None,
|
|
325
|
+
bbox_dir: Path | None = None,
|
|
323
326
|
) -> None:
|
|
324
327
|
"""Run multi-view prediction on a list of paths (videos or directories).
|
|
325
328
|
|
|
@@ -329,6 +332,9 @@ def _predict_multi_type_multi_view(
|
|
|
329
332
|
skip_viz: if True, skip generating labeled visualization outputs.
|
|
330
333
|
skip_existing: if True, skip sessions for which output CSVs already exist.
|
|
331
334
|
progress_file: optional path to write prediction progress as JSON.
|
|
335
|
+
reader: optional video-reading backend; None auto-selects.
|
|
336
|
+
bbox_dir: optional directory containing a ``<video_stem>_bbox.csv`` for every view's
|
|
337
|
+
video. When provided, each view is cropped to its bboxes before inference.
|
|
332
338
|
"""
|
|
333
339
|
# delay this import because it's slow
|
|
334
340
|
from lightning_pose.utils.io import (
|
|
@@ -362,11 +368,21 @@ def _predict_multi_type_multi_view(
|
|
|
362
368
|
logger.info(f'skipping {session_name} (prediction file already exists)')
|
|
363
369
|
continue
|
|
364
370
|
|
|
371
|
+
bbox_file_per_view = None
|
|
372
|
+
if bbox_dir is not None:
|
|
373
|
+
bbox_file_per_view = [
|
|
374
|
+
bbox_dir / f'{Path(video).stem}_bbox.csv' for video in video_file_per_view
|
|
375
|
+
]
|
|
376
|
+
missing = [f for f in bbox_file_per_view if not f.exists()]
|
|
377
|
+
if missing:
|
|
378
|
+
raise FileNotFoundError(f'bbox file(s) not found: {missing}')
|
|
379
|
+
|
|
365
380
|
model.predict_on_video_file_multiview(
|
|
366
381
|
video_file_per_view,
|
|
367
382
|
generate_labeled_video=not skip_viz,
|
|
368
383
|
progress_file=progress_file,
|
|
369
384
|
reader=reader,
|
|
385
|
+
bbox_file_per_view=bbox_file_per_view,
|
|
370
386
|
)
|
|
371
387
|
# if we have a list of directories, we process the videos in each separately
|
|
372
388
|
elif all(path.is_dir() for path in paths):
|
|
@@ -379,7 +395,7 @@ def _predict_multi_type_multi_view(
|
|
|
379
395
|
|
|
380
396
|
_predict_multi_type_multi_view(
|
|
381
397
|
model, video_files, skip_viz, skip_existing,
|
|
382
|
-
progress_file=progress_file, reader=reader,
|
|
398
|
+
progress_file=progress_file, reader=reader, bbox_dir=bbox_dir,
|
|
383
399
|
)
|
|
384
400
|
else:
|
|
385
401
|
logger.info(f'skipping {path}: no videos found')
|
|
@@ -24,6 +24,15 @@ dimensions; handle both single-view and multi-view):
|
|
|
24
24
|
|
|
25
25
|
- :func:`frame_to_model_batch` — frame → model (called before loss computation)
|
|
26
26
|
- :func:`model_to_frame_batch` — model → frame (called after inference)
|
|
27
|
+
|
|
28
|
+
Video-reader helpers (used by all three readers in ``lightning_pose.data.video`` to crop
|
|
29
|
+
per-frame bboxes out of full-resolution frames before inference):
|
|
30
|
+
|
|
31
|
+
- :func:`as_bbox_df_list` — normalize a reader's ``bbox_df`` to one DataFrame per view
|
|
32
|
+
- :func:`slice_bbox_rows` — select (and pad) the bbox rows for one batch
|
|
33
|
+
- :func:`crop_and_resize_frames` — crop + resize one view's frames to its bboxes
|
|
34
|
+
- :func:`crop_views` — apply the above to every view of a batch independently
|
|
35
|
+
- :func:`full_frame_bbox` — default bbox covering the whole decoded frame
|
|
27
36
|
"""
|
|
28
37
|
|
|
29
38
|
import pandas as pd
|
|
@@ -288,6 +297,24 @@ def model_to_frame_batch(
|
|
|
288
297
|
return model_keypoints_.reshape((-1, num_targets))
|
|
289
298
|
|
|
290
299
|
|
|
300
|
+
def slice_bbox_rows(bbox_df: pd.DataFrame, idx_start: int, seq_len: int) -> pd.DataFrame:
|
|
301
|
+
"""Select the bbox rows for one batch, padding a short final batch with the last row.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
bbox_df: DataFrame with columns ``["x", "y", "h", "w"]``, one row per video frame.
|
|
305
|
+
idx_start: index of the first frame in the batch.
|
|
306
|
+
seq_len: number of frames in the batch.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
DataFrame with exactly ``seq_len`` rows, ready for :func:`crop_and_resize_frames`.
|
|
310
|
+
"""
|
|
311
|
+
rows = bbox_df.iloc[idx_start:idx_start + seq_len]
|
|
312
|
+
if len(rows) < seq_len:
|
|
313
|
+
last_row = bbox_df.iloc[[-1]]
|
|
314
|
+
rows = pd.concat([rows] + [last_row] * (seq_len - len(rows)), ignore_index=True)
|
|
315
|
+
return rows
|
|
316
|
+
|
|
317
|
+
|
|
291
318
|
def crop_and_resize_frames(
|
|
292
319
|
frames: Float[torch.Tensor, "seq_len rgb h w"],
|
|
293
320
|
bbox_rows: pd.DataFrame,
|
|
@@ -299,9 +326,10 @@ def crop_and_resize_frames(
|
|
|
299
326
|
"""Crop each frame to its per-frame bounding box and resize to a common size.
|
|
300
327
|
|
|
301
328
|
Shared by all video-decoding backends: each hands this function a full-resolution ``frames``
|
|
302
|
-
tensor plus one already-sliced-and-padded bbox row per frame
|
|
303
|
-
|
|
304
|
-
see
|
|
329
|
+
tensor plus one already-sliced-and-padded bbox row per frame. Row selection is done by
|
|
330
|
+
:func:`slice_bbox_rows` (via :func:`crop_views`); the cursor/step bookkeeping that decides
|
|
331
|
+
where a batch starts lives in each reader's wrapper, not here (see
|
|
332
|
+
``LitDaliWrapper._dali_output_to_tensors`` and ``LitPredictWrapperBase.__next__``).
|
|
305
333
|
|
|
306
334
|
Args:
|
|
307
335
|
frames: full-resolution frames, shape (seq_len, 3, H, W).
|
|
@@ -341,3 +369,83 @@ def crop_and_resize_frames(
|
|
|
341
369
|
)
|
|
342
370
|
)
|
|
343
371
|
return torch.stack(cropped_frames), torch.stack(bboxes)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def as_bbox_df_list(
|
|
375
|
+
bbox_df: pd.DataFrame | list[pd.DataFrame] | None,
|
|
376
|
+
num_views: int,
|
|
377
|
+
) -> list[pd.DataFrame] | None:
|
|
378
|
+
"""Normalize a video reader's ``bbox_df`` to a list with one DataFrame per view.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
bbox_df: a single DataFrame (single-view), a list with one DataFrame per view
|
|
382
|
+
(multiview), or None when no bbox cropping is configured.
|
|
383
|
+
num_views: number of views in the batch being built (1 for single-view).
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
list of per-view bbox DataFrames, or None if ``bbox_df`` is None.
|
|
387
|
+
|
|
388
|
+
Raises:
|
|
389
|
+
ValueError: if the number of DataFrames does not match ``num_views``.
|
|
390
|
+
"""
|
|
391
|
+
if bbox_df is None:
|
|
392
|
+
return None
|
|
393
|
+
bbox_dfs = [bbox_df] if isinstance(bbox_df, pd.DataFrame) else list(bbox_df)
|
|
394
|
+
if len(bbox_dfs) != num_views:
|
|
395
|
+
raise ValueError(f'expected {num_views} bbox DataFrame(s), got {len(bbox_dfs)}')
|
|
396
|
+
return bbox_dfs
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def full_frame_bbox(
|
|
400
|
+
height: int,
|
|
401
|
+
width: int,
|
|
402
|
+
seq_len: int,
|
|
403
|
+
device: torch.device | str,
|
|
404
|
+
) -> Float[torch.Tensor, "seq_len xyhw"]:
|
|
405
|
+
"""Bbox covering an entire decoded frame (x=0, y=0, h=height, w=width), per frame.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
height: height of the decoded (pre-resize) frame in pixels.
|
|
409
|
+
width: width of the decoded (pre-resize) frame in pixels.
|
|
410
|
+
seq_len: number of frames in the batch.
|
|
411
|
+
device: device to create the tensor on.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
float32 tensor of shape (seq_len, 4) in [x, y, h, w] order.
|
|
415
|
+
"""
|
|
416
|
+
return torch.tensor([0, 0, height, width], dtype=torch.float32, device=device).repeat(
|
|
417
|
+
seq_len, 1,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def crop_views(
|
|
422
|
+
frames_list: list[torch.Tensor],
|
|
423
|
+
bbox_dfs: list[pd.DataFrame],
|
|
424
|
+
idx_start: int,
|
|
425
|
+
resize_dims: list[int],
|
|
426
|
+
) -> tuple[list[torch.Tensor], list[torch.Tensor]]:
|
|
427
|
+
"""Crop and resize each view of a batch to its own per-frame bboxes.
|
|
428
|
+
|
|
429
|
+
Views are processed independently, so they may have different native resolutions; after
|
|
430
|
+
the crop+resize every view has shape ``resize_dims`` and the results can be stacked.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
frames_list: full-resolution frames, one tensor of shape (seq_len, 3, H, W) per view.
|
|
434
|
+
bbox_dfs: one bbox DataFrame per view (columns ``["x", "y", "h", "w"]``, one row per
|
|
435
|
+
video frame), in the same order as ``frames_list``.
|
|
436
|
+
idx_start: video frame index of the first frame in this batch.
|
|
437
|
+
resize_dims: target ``[height, width]`` to resize each crop to.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
Tuple of (cropped+resized frames per view, actual bbox coordinates used per view); each
|
|
441
|
+
frames tensor has shape (seq_len, 3, *resize_dims) and each bbox tensor (seq_len, 4).
|
|
442
|
+
"""
|
|
443
|
+
seq_len = frames_list[0].shape[0]
|
|
444
|
+
cropped_list = []
|
|
445
|
+
bbox_list = []
|
|
446
|
+
for frames, bbox_df in zip(frames_list, bbox_dfs, strict=True):
|
|
447
|
+
rows = slice_bbox_rows(bbox_df, idx_start, seq_len)
|
|
448
|
+
cropped, bboxes = crop_and_resize_frames(frames, rows, resize_dims)
|
|
449
|
+
cropped_list.append(cropped)
|
|
450
|
+
bbox_list.append(bboxes)
|
|
451
|
+
return cropped_list, bbox_list
|
|
@@ -26,6 +26,12 @@ Backends
|
|
|
26
26
|
``opencv-python-headless``, an unconditional cross-platform dependency -- works on any
|
|
27
27
|
platform/hardware, including macOS, Windows, and GPU-less machines. Slower than DALI/pynvvc on
|
|
28
28
|
a supported NVIDIA box; the guaranteed fallback everywhere else. Prediction only.
|
|
29
|
+
- ``utils.py`` -- :class:`~lightning_pose.data.video.utils.PreparePredictBase` /
|
|
30
|
+
:class:`~lightning_pose.data.video.utils.LitPredictWrapperBase`, the validation, windowing,
|
|
31
|
+
normalization, bbox-crop and batch-assembly logic shared by the two prediction-only backends
|
|
32
|
+
(``pynvvc.py`` and ``opencv.py`` implement only decoding); also the batch-assembly and
|
|
33
|
+
normalization helper functions that ``dali.py`` reuses. Imports neither DALI nor
|
|
34
|
+
PyNvVideoCodec.
|
|
29
35
|
- ``factory.py`` -- :func:`~lightning_pose.data.video.factory.build_video_reader`, the reader-
|
|
30
36
|
selection dispatch used by ``predict_video``. See its docstring for the fallback chain and
|
|
31
37
|
the recipe for adding a new backend.
|
|
@@ -29,10 +29,14 @@ Standard mode (no ``bbox_df``):
|
|
|
29
29
|
|
|
30
30
|
Bbox-crop mode (``bbox_df`` supplied to ``PrepareDALI``):
|
|
31
31
|
DALI delivers full-resolution frames (``resize_dims=None`` in the predict pipe) so that
|
|
32
|
-
``LitDaliWrapper.
|
|
33
|
-
and resize to the original ``resize_dims`` using ``torch.nn.functional.interpolate``.
|
|
32
|
+
``LitDaliWrapper._dali_output_to_tensors`` can crop each frame to its per-frame bounding
|
|
33
|
+
box and resize to the original ``resize_dims`` using ``torch.nn.functional.interpolate``.
|
|
34
34
|
The ``bbox`` field of each batch contains the actual crop coordinates, so downstream
|
|
35
|
-
code can remap predictions back to the original coordinate space.
|
|
35
|
+
code can remap predictions back to the original coordinate space. For multiview, one
|
|
36
|
+
bbox DataFrame per view is supplied and each view is cropped independently, so views may
|
|
37
|
+
have different native resolutions. The crop and batch assembly use the helpers shared with
|
|
38
|
+
the other readers (``lightning_pose.data.bboxes.crop_views``,
|
|
39
|
+
``lightning_pose.data.video.utils.build_batch_dict``).
|
|
36
40
|
"""
|
|
37
41
|
|
|
38
42
|
import os
|
|
@@ -55,12 +59,13 @@ except ImportError as e:
|
|
|
55
59
|
) from e
|
|
56
60
|
|
|
57
61
|
from lightning_pose.data import _IMAGENET_MEAN, _IMAGENET_STD
|
|
58
|
-
from lightning_pose.data.bboxes import
|
|
62
|
+
from lightning_pose.data.bboxes import as_bbox_df_list, crop_views, full_frame_bbox
|
|
59
63
|
from lightning_pose.data.datatypes import (
|
|
60
64
|
MultiviewUnlabeledBatchDict,
|
|
61
65
|
UnlabeledBatchDict,
|
|
62
66
|
)
|
|
63
67
|
from lightning_pose.data.utils import count_frames
|
|
68
|
+
from lightning_pose.data.video.utils import build_batch_dict
|
|
64
69
|
|
|
65
70
|
# to ignore imports for sphinx-autoapidoc
|
|
66
71
|
__all__: list[str] = []
|
|
@@ -206,7 +211,7 @@ class LitDaliWrapper(DALIGenericIterator):
|
|
|
206
211
|
to the model's input dimensions before being returned.
|
|
207
212
|
|
|
208
213
|
``_frame_idx`` tracks the iterator's position in the video (by frame number) so that
|
|
209
|
-
``
|
|
214
|
+
``_dali_output_to_tensors`` reads the correct rows from ``bbox_df``. It advances by
|
|
210
215
|
``seq_len`` for base models (non-overlapping windows) and by ``seq_len - 4`` for
|
|
211
216
|
context models, because the DALI reader for context prediction uses a step of
|
|
212
217
|
``seq_len - 4`` so that consecutive 5-frame windows overlap by 4 frames.
|
|
@@ -214,7 +219,7 @@ class LitDaliWrapper(DALIGenericIterator):
|
|
|
214
219
|
Testing without a GPU
|
|
215
220
|
+++++++++++++++++++++
|
|
216
221
|
This class can be instantiated without a real DALI pipeline or GPU for unit tests
|
|
217
|
-
that only exercise the PyTorch post-processing logic (e.g. ``
|
|
222
|
+
that only exercise the PyTorch post-processing logic (e.g. ``_dali_output_to_tensors``)::
|
|
218
223
|
|
|
219
224
|
wrapper = object.__new__(LitDaliWrapper)
|
|
220
225
|
wrapper.do_context = False
|
|
@@ -229,7 +234,7 @@ class LitDaliWrapper(DALIGenericIterator):
|
|
|
229
234
|
eval_mode: Literal["train", "predict"],
|
|
230
235
|
num_iters: int = 1,
|
|
231
236
|
do_context: bool = False,
|
|
232
|
-
bbox_df: pd.DataFrame | None = None,
|
|
237
|
+
bbox_df: pd.DataFrame | list[pd.DataFrame] | None = None,
|
|
233
238
|
resize_dims: list[int] | None = None,
|
|
234
239
|
**kwargs: Any,
|
|
235
240
|
) -> None:
|
|
@@ -241,7 +246,8 @@ class LitDaliWrapper(DALIGenericIterator):
|
|
|
241
246
|
should be fixed by lightning/dali teams)
|
|
242
247
|
do_context: whether model/loader use 5-frame context or not
|
|
243
248
|
bbox_df: optional DataFrame with columns ``["x", "y", "h", "w"]``, one row per
|
|
244
|
-
frame
|
|
249
|
+
frame (or, for multiview, a list with one such DataFrame per view, in view
|
|
250
|
+
order). When provided, each batch's frames are cropped per-frame and resized
|
|
245
251
|
to ``resize_dims`` before being returned, and the ``bbox`` field of the
|
|
246
252
|
batch dict is populated with the actual bbox coordinates.
|
|
247
253
|
resize_dims: target ``[height, width]`` for post-crop resize; required when
|
|
@@ -264,11 +270,30 @@ class LitDaliWrapper(DALIGenericIterator):
|
|
|
264
270
|
return self.num_iters
|
|
265
271
|
|
|
266
272
|
@staticmethod
|
|
273
|
+
def _frame_size_hw(frame_size: torch.Tensor) -> tuple[int, int]:
|
|
274
|
+
"""Extract (height, width) of the decoded frames from DALI's ``frame_size`` output.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
frame_size: DALI ``frame_size`` output for one view, shape (1, 4) with either
|
|
278
|
+
``seq_len,H,W,C`` or ``seq_len,C,H,W`` ordering.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Tuple of (height, width) in pixels.
|
|
282
|
+
"""
|
|
283
|
+
if frame_size[0, -1] == 3: # order is seq_len,H,W,C
|
|
284
|
+
return int(frame_size[0, 1]), int(frame_size[0, 2])
|
|
285
|
+
return int(frame_size[0, 2]), int(frame_size[0, 3]) # order is seq_len,C,H,W
|
|
286
|
+
|
|
267
287
|
def _dali_output_to_tensors(
|
|
268
|
-
batch: list
|
|
288
|
+
self, batch: list
|
|
269
289
|
) -> UnlabeledBatchDict | MultiviewUnlabeledBatchDict:
|
|
270
290
|
"""Convert a raw DALI batch output to a typed batch dictionary.
|
|
271
291
|
|
|
292
|
+
When ``self.bbox_df`` is set, each view is cropped to its per-frame bbox and resized
|
|
293
|
+
to ``self.resize_dims`` independently, so views may have different native resolutions.
|
|
294
|
+
Otherwise DALI has already resized the frames, and the ``bbox`` field spans the full
|
|
295
|
+
decoded frame (x=0, y=0, h=H, w=W) of each view.
|
|
296
|
+
|
|
272
297
|
Args:
|
|
273
298
|
batch: raw output list from DALI's ``DALIGenericIterator.__next__``.
|
|
274
299
|
|
|
@@ -277,107 +302,50 @@ class LitDaliWrapper(DALIGenericIterator):
|
|
|
277
302
|
``MultiviewUnlabeledBatchDict`` for multi-view pipelines.
|
|
278
303
|
"""
|
|
279
304
|
# always batch_size=1
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
height = batch[0]["frame_size"][0, 2]
|
|
293
|
-
width = batch[0]["frame_size"][0, 3]
|
|
294
|
-
bbox = torch.tensor([0, 0, height, width], device=frames.device).repeat(
|
|
295
|
-
(frames.shape[0], 1))
|
|
296
|
-
|
|
297
|
-
return UnlabeledBatchDict(
|
|
298
|
-
frames=frames, transforms=transforms, bbox=bbox, is_multiview=False,
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
else: # multiview pipeline
|
|
302
|
-
|
|
303
|
-
# final shape: ("seq_len", "num_views", "RGB":3, "image_height", "image_width")
|
|
304
|
-
frames = torch.stack(
|
|
305
|
-
[batch[0][key][0, :, :, :, :] for key in batch[0].keys() if
|
|
306
|
-
"transforms" not in key and "frame_size" not in key],
|
|
307
|
-
dim=1,
|
|
308
|
-
)
|
|
309
|
-
|
|
305
|
+
output = batch[0]
|
|
306
|
+
is_multiview = len(output.keys()) != 3
|
|
307
|
+
|
|
308
|
+
# per-view lists of frames (each of shape (seq_len, 3, H, W)) and decoded frame sizes
|
|
309
|
+
if is_multiview:
|
|
310
|
+
frames_list = [
|
|
311
|
+
output[key][0] for key in output.keys()
|
|
312
|
+
if 'transforms' not in key and 'frame_size' not in key
|
|
313
|
+
]
|
|
314
|
+
frame_sizes = [
|
|
315
|
+
self._frame_size_hw(output[key]) for key in output.keys() if 'frame_size' in key
|
|
316
|
+
]
|
|
310
317
|
# final shape: ("num_views", "h":2, "w":3)
|
|
311
318
|
transforms = torch.stack(
|
|
312
|
-
[
|
|
313
|
-
dim=0,
|
|
319
|
+
[output[key] for key in output.keys() if 'transforms' in key], dim=0,
|
|
314
320
|
)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def _apply_bbox_crop(self, batch_dict: UnlabeledBatchDict) -> UnlabeledBatchDict:
|
|
333
|
-
"""Crop frames to per-frame bboxes and resize to the model's input dimensions.
|
|
334
|
-
|
|
335
|
-
Called only in bbox-crop mode (when ``bbox_df`` is not None). DALI has already
|
|
336
|
-
delivered full-resolution frames; this method crops each frame individually,
|
|
337
|
-
resizes to ``self.resize_dims``, and advances ``_frame_idx`` by the same step
|
|
338
|
-
that the DALI reader used, so the two cursors stay in sync.
|
|
339
|
-
|
|
340
|
-
Args:
|
|
341
|
-
batch_dict: single-view unlabeled batch with full-resolution frames from DALI.
|
|
342
|
-
|
|
343
|
-
Returns:
|
|
344
|
-
new ``UnlabeledBatchDict`` with cropped+resized frames and the actual bbox
|
|
345
|
-
coordinates in the ``bbox`` field (shape ``(seq_len, 4)``, order x y h w).
|
|
346
|
-
"""
|
|
347
|
-
assert self.bbox_df is not None # guarded by caller; assertion narrows type
|
|
348
|
-
frames = batch_dict['frames'] # (seq_len, 3, H, W)
|
|
349
|
-
seq_len = frames.shape[0]
|
|
350
|
-
# context-model DALI reader uses step=seq_len-4 so consecutive windows overlap by 4
|
|
351
|
-
# frames; _frame_idx must advance by the same amount to stay in sync with the reader
|
|
352
|
-
step = seq_len - 4 if self.do_context else seq_len
|
|
353
|
-
|
|
354
|
-
# slice bbox rows for this batch; pad last partial batch with the final row
|
|
355
|
-
rows = self.bbox_df.iloc[self._frame_idx:self._frame_idx + seq_len]
|
|
356
|
-
if len(rows) < seq_len:
|
|
357
|
-
last_row = self.bbox_df.iloc[[-1]]
|
|
358
|
-
rows = pd.concat(
|
|
359
|
-
[rows] + [last_row] * (seq_len - len(rows)),
|
|
360
|
-
ignore_index=True,
|
|
321
|
+
else:
|
|
322
|
+
frames_list = [output['frames'][0]]
|
|
323
|
+
frame_sizes = [self._frame_size_hw(output['frame_size'])]
|
|
324
|
+
# shape (1,) or (2, 3)
|
|
325
|
+
transforms = output['transforms'][0]
|
|
326
|
+
|
|
327
|
+
seq_len = frames_list[0].shape[0]
|
|
328
|
+
bbox_dfs = as_bbox_df_list(self.bbox_df, len(frames_list))
|
|
329
|
+
if bbox_dfs is None:
|
|
330
|
+
bbox_list = [
|
|
331
|
+
full_frame_bbox(height, width, seq_len, frames_list[0].device)
|
|
332
|
+
for height, width in frame_sizes
|
|
333
|
+
]
|
|
334
|
+
else:
|
|
335
|
+
assert self.resize_dims is not None # required when bbox_df set, see __init__
|
|
336
|
+
frames_list, bbox_list = crop_views(
|
|
337
|
+
frames_list, bbox_dfs, self._frame_idx, self.resize_dims,
|
|
361
338
|
)
|
|
339
|
+
# context-model DALI reader uses step=seq_len-4 so consecutive windows overlap by 4
|
|
340
|
+
# frames; _frame_idx must advance by the same amount to stay in sync with the reader
|
|
341
|
+
self._frame_idx += seq_len - 4 if self.do_context else seq_len
|
|
362
342
|
|
|
363
|
-
|
|
364
|
-
cropped_frames, bboxes = crop_and_resize_frames(frames, rows, self.resize_dims)
|
|
365
|
-
self._frame_idx += step
|
|
366
|
-
|
|
367
|
-
return UnlabeledBatchDict(
|
|
368
|
-
frames=cropped_frames,
|
|
369
|
-
transforms=batch_dict['transforms'],
|
|
370
|
-
bbox=bboxes,
|
|
371
|
-
is_multiview=False,
|
|
372
|
-
)
|
|
343
|
+
return build_batch_dict(frames_list, bbox_list, transforms, is_multiview)
|
|
373
344
|
|
|
374
345
|
def __next__(self) -> UnlabeledBatchDict | MultiviewUnlabeledBatchDict:
|
|
375
346
|
"""Fetch the next batch, applying per-frame bbox crop+resize when configured."""
|
|
376
347
|
batch = super().__next__()
|
|
377
|
-
|
|
378
|
-
if self.bbox_df is not None and not batch_dict['is_multiview']:
|
|
379
|
-
return self._apply_bbox_crop(batch_dict) # type: ignore[arg-type]
|
|
380
|
-
return batch_dict
|
|
348
|
+
return self._dali_output_to_tensors(batch=batch)
|
|
381
349
|
|
|
382
350
|
|
|
383
351
|
class PrepareDALI:
|
|
@@ -405,7 +373,7 @@ class PrepareDALI:
|
|
|
405
373
|
dali_config: dict | DictConfig | ListConfig | None = None,
|
|
406
374
|
imgaug: str | None = "default",
|
|
407
375
|
num_threads: int = 1,
|
|
408
|
-
bbox_df: pd.DataFrame | None = None,
|
|
376
|
+
bbox_df: pd.DataFrame | list[pd.DataFrame] | None = None,
|
|
409
377
|
) -> None:
|
|
410
378
|
"""Initialize DALI pipelines and dataloaders for training or prediction.
|
|
411
379
|
|
|
@@ -422,15 +390,17 @@ class PrepareDALI:
|
|
|
422
390
|
``"dlc"``); pass ``"default"`` for resize-only or ``None`` to disable.
|
|
423
391
|
num_threads: number of CPU threads used by DALI pipelines.
|
|
424
392
|
bbox_df: optional DataFrame with columns ``["x", "y", "h", "w"]``, one row per
|
|
425
|
-
frame
|
|
426
|
-
|
|
427
|
-
|
|
393
|
+
frame; for multiview, a list with one such DataFrame per view, in the same
|
|
394
|
+
order as ``filenames``. When provided, the predict pipeline loads
|
|
395
|
+
full-resolution frames (DALI resize is disabled) and ``LitDaliWrapper``
|
|
396
|
+
crops each frame to its bbox before resizing to ``resize_dims``.
|
|
428
397
|
|
|
429
398
|
Raises:
|
|
430
399
|
FileNotFoundError: if any path in ``filenames`` does not exist or is not a file.
|
|
431
400
|
ValueError: for multiview inputs, if views have differing numbers of sessions or if a
|
|
432
401
|
session has differing frame counts across views (which would desynchronize the
|
|
433
|
-
per-view readers)
|
|
402
|
+
per-view readers); if the number of ``bbox_df`` DataFrames does not match the
|
|
403
|
+
number of views.
|
|
434
404
|
"""
|
|
435
405
|
# determine if we have a multiview pipeline
|
|
436
406
|
if isinstance(filenames, list) and isinstance(filenames[0], list):
|
|
@@ -449,6 +419,14 @@ class PrepareDALI:
|
|
|
449
419
|
if not os.path.exists(vid) or not os.path.isfile(vid):
|
|
450
420
|
raise FileNotFoundError(f"{vid} is not a video file!")
|
|
451
421
|
|
|
422
|
+
if bbox_df is not None:
|
|
423
|
+
num_bbox_dfs = 1 if isinstance(bbox_df, pd.DataFrame) else len(bbox_df)
|
|
424
|
+
if num_bbox_dfs != len(filenames_2d):
|
|
425
|
+
raise ValueError(
|
|
426
|
+
f'got {num_bbox_dfs} bbox DataFrame(s) for {len(filenames_2d)} view(s); '
|
|
427
|
+
'multiview requires a list with one bbox DataFrame per view'
|
|
428
|
+
)
|
|
429
|
+
|
|
452
430
|
# frame counts of view 0, per session; reused below for `self.frame_count`
|
|
453
431
|
view0_frame_counts = list(map(count_frames, filenames_2d[0]))
|
|
454
432
|
|
|
@@ -549,7 +527,7 @@ class PrepareDALI:
|
|
|
549
527
|
|
|
550
528
|
When ``bbox_df`` is set, predict entries have ``resize_dims`` forced to ``None``
|
|
551
529
|
so DALI delivers full-resolution frames. The per-frame resize happens in PyTorch
|
|
552
|
-
inside ``LitDaliWrapper.
|
|
530
|
+
inside ``LitDaliWrapper._dali_output_to_tensors`` after cropping, using the original
|
|
553
531
|
``resize_dims`` stored on the wrapper.
|
|
554
532
|
"""
|
|
555
533
|
assert self.dali_config is not None
|
|
@@ -9,7 +9,9 @@ Adding a new video reader
|
|
|
9
9
|
-------------------------
|
|
10
10
|
|
|
11
11
|
Every backend module (``dali.py``, ``pynvvc.py``, ``opencv.py``, ...) implements the
|
|
12
|
-
same contract.
|
|
12
|
+
same contract. A prediction-only backend can subclass ``PreparePredictBase`` and
|
|
13
|
+
``LitPredictWrapperBase`` from ``utils.py`` and implement only decoding (see ``opencv.py``).
|
|
14
|
+
To add one:
|
|
13
15
|
|
|
14
16
|
1. **Two-phase prepare class**: ``Prepare<Name>.__init__(model_type, filenames,
|
|
15
17
|
resize_dims, dali_config, bbox_df=None, ...)`` validates inputs and precomputes
|
|
@@ -83,7 +85,7 @@ def build_video_reader(
|
|
|
83
85
|
dali_config: dict | DictConfig | ListConfig,
|
|
84
86
|
filenames: list[str] | list[list[str]],
|
|
85
87
|
resize_dims: list[int],
|
|
86
|
-
bbox_df: pd.DataFrame | None,
|
|
88
|
+
bbox_df: pd.DataFrame | list[pd.DataFrame] | None,
|
|
87
89
|
) -> LitDaliWrapper | LitPynvvcWrapper | LitOpenCVWrapper:
|
|
88
90
|
"""Resolve which video-reading backend to use and build its dataloader.
|
|
89
91
|
|
|
@@ -109,7 +111,8 @@ def build_video_reader(
|
|
|
109
111
|
multiview, one single-element list per view (see each backend's
|
|
110
112
|
``Prepare<Name>`` class for the exact shape).
|
|
111
113
|
resize_dims: ``[height, width]`` the model expects as input.
|
|
112
|
-
bbox_df: optional per-frame bbox DataFrame;
|
|
114
|
+
bbox_df: optional per-frame bbox DataFrame; for multiview, a list with one
|
|
115
|
+
DataFrame per view (same order as ``filenames``).
|
|
113
116
|
|
|
114
117
|
Returns:
|
|
115
118
|
A ready-to-iterate loader (``LitDaliWrapper``, ``LitPynvvcWrapper``, or
|