mvdata 0.9.0__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.
- mvdata-0.9.0/PKG-INFO +52 -0
- mvdata-0.9.0/README.md +6 -0
- mvdata-0.9.0/mvdata/__init__.py +169 -0
- mvdata-0.9.0/mvdata/cloud_storage.py +204 -0
- mvdata-0.9.0/mvdata/codec/__init__.py +126 -0
- mvdata-0.9.0/mvdata/codec/_imports.py +42 -0
- mvdata-0.9.0/mvdata/codec/decode.py +123 -0
- mvdata-0.9.0/mvdata/codec/encode.py +313 -0
- mvdata-0.9.0/mvdata/codec/frames.py +199 -0
- mvdata-0.9.0/mvdata/codec/probe.py +239 -0
- mvdata-0.9.0/mvdata/codec/select.py +153 -0
- mvdata-0.9.0/mvdata/dataset_base.py +1049 -0
- mvdata-0.9.0/mvdata/downloader.py +806 -0
- mvdata-0.9.0/mvdata/gpu_policy.py +128 -0
- mvdata-0.9.0/mvdata/gpu_support.py +384 -0
- mvdata-0.9.0/mvdata/image_metrics.py +24 -0
- mvdata-0.9.0/mvdata/legacy_writer.py +371 -0
- mvdata-0.9.0/mvdata/multivideo.py +982 -0
- mvdata-0.9.0/mvdata/multivideo_decode_benchmark.py +216 -0
- mvdata-0.9.0/mvdata/multivideo_slicer.py +930 -0
- mvdata-0.9.0/mvdata/multivideo_writer.py +375 -0
- mvdata-0.9.0/mvdata/nvdec_parallel.py +145 -0
- mvdata-0.9.0/mvdata/nvenc_codec.py +17 -0
- mvdata-0.9.0/mvdata/per_frame.py +1799 -0
- mvdata-0.9.0/mvdata/ranged.py +1349 -0
- mvdata-0.9.0/mvdata/ranged_writer.py +964 -0
- mvdata-0.9.0/mvdata/stash_utils.py +358 -0
- mvdata-0.9.0/mvdata/utils.py +33 -0
- mvdata-0.9.0/mvdata/video_stream_reader.py +490 -0
- mvdata-0.9.0/mvdata/write_progress.py +213 -0
- mvdata-0.9.0/mvdata/writer_base.py +161 -0
- mvdata-0.9.0/mvdata.egg-info/PKG-INFO +52 -0
- mvdata-0.9.0/mvdata.egg-info/SOURCES.txt +57 -0
- mvdata-0.9.0/mvdata.egg-info/dependency_links.txt +1 -0
- mvdata-0.9.0/mvdata.egg-info/requires.txt +29 -0
- mvdata-0.9.0/mvdata.egg-info/top_level.txt +1 -0
- mvdata-0.9.0/pyproject.toml +160 -0
- mvdata-0.9.0/setup.cfg +4 -0
- mvdata-0.9.0/tests/test_dataset_base_defaults.py +203 -0
- mvdata-0.9.0/tests/test_gpu_policy.py +62 -0
- mvdata-0.9.0/tests/test_gpu_support.py +126 -0
- mvdata-0.9.0/tests/test_image_metrics.py +18 -0
- mvdata-0.9.0/tests/test_multivideo_bit_depth.py +102 -0
- mvdata-0.9.0/tests/test_multivideo_decode_benchmark.py +127 -0
- mvdata-0.9.0/tests/test_multivideo_slicer.py +223 -0
- mvdata-0.9.0/tests/test_nvdec_parallel.py +57 -0
- mvdata-0.9.0/tests/test_per_camera.py +44 -0
- mvdata-0.9.0/tests/test_ranged_mixed_streams.py +467 -0
- mvdata-0.9.0/tests/test_ranged_nvenc_roundtrip.py +468 -0
- mvdata-0.9.0/tests/test_ranged_resume.py +453 -0
- mvdata-0.9.0/tests/test_ranged_stream_discovery.py +27 -0
- mvdata-0.9.0/tests/test_roundtrip.py +385 -0
- mvdata-0.9.0/tests/test_s3_downloader.py +287 -0
- mvdata-0.9.0/tests/test_stash_bit_depth.py +114 -0
- mvdata-0.9.0/tests/test_stash_comprehensive.py +754 -0
- mvdata-0.9.0/tests/test_stash_policy.py +158 -0
- mvdata-0.9.0/tests/test_stash_regenerate.py +520 -0
- mvdata-0.9.0/tests/test_video_stream_reader.py +875 -0
- mvdata-0.9.0/tests/test_write_progress.py +33 -0
mvdata-0.9.0/PKG-INFO
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mvdata
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Gracia Dataset Convention - Python library for working with multi-view video datasets
|
|
5
|
+
Author: Gracia Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/gracia-labs/mvdata
|
|
8
|
+
Project-URL: Documentation, https://github.com/gracia-labs/mvdata/tree/main/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/gracia-labs/mvdata
|
|
10
|
+
Project-URL: Issues, https://github.com/gracia-labs/mvdata/issues
|
|
11
|
+
Keywords: dataset,multi-view,video,avif,computer-vision
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
20
|
+
Classifier: Topic :: Multimedia :: Video
|
|
21
|
+
Requires-Python: <3.13,>=3.12
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: numpy>=1.20.0
|
|
24
|
+
Requires-Dist: Pillow>=8.0.0
|
|
25
|
+
Requires-Dist: pyavif>=0.0.2
|
|
26
|
+
Requires-Dist: av>=10.0.0
|
|
27
|
+
Provides-Extra: pynvc
|
|
28
|
+
Requires-Dist: PyNvVideoCodec>=2.1.0; extra == "pynvc"
|
|
29
|
+
Provides-Extra: cuda
|
|
30
|
+
Requires-Dist: cupy-cuda12x>=13.0.0; extra == "cuda"
|
|
31
|
+
Provides-Extra: s3
|
|
32
|
+
Requires-Dist: boto3>=1.26.0; extra == "s3"
|
|
33
|
+
Provides-Extra: gcs
|
|
34
|
+
Requires-Dist: google-cloud-storage>=2.0.0; extra == "gcs"
|
|
35
|
+
Provides-Extra: cloud
|
|
36
|
+
Requires-Dist: boto3>=1.26.0; extra == "cloud"
|
|
37
|
+
Requires-Dist: google-cloud-storage>=2.0.0; extra == "cloud"
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: ipykernel>=7.2.0; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
41
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
42
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
43
|
+
Requires-Dist: isort>=5.10.0; extra == "dev"
|
|
44
|
+
Requires-Dist: mypy>=0.990; extra == "dev"
|
|
45
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
46
|
+
|
|
47
|
+
# mvdata
|
|
48
|
+
|
|
49
|
+
Python library for working with Gracia multi-view video datasets.
|
|
50
|
+
|
|
51
|
+
The package provides readers, writers, conversion tools, and GPU-aware video
|
|
52
|
+
decode helpers for the dataset layouts documented in the `docs` directory.
|
mvdata-0.9.0/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gracia Dataset Convention - Python library for working with multi-view video datasets.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .cloud_storage import CloudStorageProviderType, CloudStorageUrl
|
|
8
|
+
from .dataset_base import Dataset, FrameReader, auto_detect_dataset
|
|
9
|
+
from .downloader import (
|
|
10
|
+
DatasetDownloader,
|
|
11
|
+
LegacyDatasetDownloader,
|
|
12
|
+
MultiVideoDatasetDownloader,
|
|
13
|
+
RangedDatasetDownloader,
|
|
14
|
+
infer_and_download_dataset,
|
|
15
|
+
register_downloader,
|
|
16
|
+
)
|
|
17
|
+
from .legacy_writer import LegacyDatasetWriter
|
|
18
|
+
from .per_frame import LegacyDataset, LegacyFrameReader
|
|
19
|
+
from .writer_base import DatasetWriter
|
|
20
|
+
from .gpu_policy import (
|
|
21
|
+
clear_gpu_override,
|
|
22
|
+
gpu_disabled,
|
|
23
|
+
gpu_enabled,
|
|
24
|
+
gpu_enabled_override,
|
|
25
|
+
gpu_usage_allowed,
|
|
26
|
+
gpu_nvenc_disabled_globally,
|
|
27
|
+
nvdec_decode_allowed,
|
|
28
|
+
nvdec_decoding_usable,
|
|
29
|
+
nvenc_encode_allowed,
|
|
30
|
+
nvenc_encoding_usable,
|
|
31
|
+
pynvvideocodec_importable,
|
|
32
|
+
)
|
|
33
|
+
from .gpu_support import (
|
|
34
|
+
GpuDecodeMediaSupport,
|
|
35
|
+
GpuDecodeSupportReport,
|
|
36
|
+
gpu_decode_support,
|
|
37
|
+
supports_gpu_decode,
|
|
38
|
+
)
|
|
39
|
+
from .image_metrics import calculate_psnr
|
|
40
|
+
from .multivideo_decode_benchmark import (
|
|
41
|
+
MultiVideoDecodeBenchmarkResult,
|
|
42
|
+
NvdecRuntimeInfo,
|
|
43
|
+
benchmark_multivideo_decode,
|
|
44
|
+
inspect_nvdec_runtime,
|
|
45
|
+
summarize_multivideo_dataset,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _build_missing_class(class_name: str, install_hint: str, import_error: Exception):
|
|
50
|
+
"""Create a placeholder class that raises a clear dependency error when instantiated."""
|
|
51
|
+
|
|
52
|
+
def _raise_missing(self, *args: Any, **kwargs: Any) -> None:
|
|
53
|
+
raise ImportError(
|
|
54
|
+
f"{class_name} is unavailable because an optional dependency is missing. "
|
|
55
|
+
f"Install it with: {install_hint}"
|
|
56
|
+
) from import_error
|
|
57
|
+
|
|
58
|
+
missing_class = type(class_name, (), {"__init__": _raise_missing})
|
|
59
|
+
missing_class.__module__ = __name__
|
|
60
|
+
return missing_class
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
from .ranged import RangedDataset, RangedFrameReader, RangedMp4FrameReader
|
|
65
|
+
from .ranged_writer import RangedDatasetWriter, RangedNvencSettings
|
|
66
|
+
except Exception as exc: # pragma: no cover - exercised in environments without pyavif
|
|
67
|
+
RangedDataset = _build_missing_class("RangedDataset", "pip install pyavif", exc)
|
|
68
|
+
RangedFrameReader = _build_missing_class("RangedFrameReader", "pip install pyavif", exc)
|
|
69
|
+
RangedMp4FrameReader = _build_missing_class("RangedMp4FrameReader", "pip install pyavif", exc)
|
|
70
|
+
RangedDatasetWriter = _build_missing_class("RangedDatasetWriter", "pip install pyavif", exc)
|
|
71
|
+
RangedNvencSettings = _build_missing_class("RangedNvencSettings", "pip install pyavif", exc)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
from .multivideo import MultiVideoDataset, MultiVideoFrameReader
|
|
75
|
+
from .multivideo_writer import MultiVideoDatasetWriter
|
|
76
|
+
except Exception as exc: # pragma: no cover - exercised in environments without av/ffmpeg
|
|
77
|
+
MultiVideoDataset = _build_missing_class("MultiVideoDataset", "pip install av", exc)
|
|
78
|
+
MultiVideoFrameReader = _build_missing_class("MultiVideoFrameReader", "pip install av", exc)
|
|
79
|
+
MultiVideoDatasetWriter = _build_missing_class("MultiVideoDatasetWriter", "pip install av", exc)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
from .multivideo_slicer import (
|
|
83
|
+
MultiVideoSliceEligibility,
|
|
84
|
+
MultiVideoSliceEligibilityMetadata,
|
|
85
|
+
MultiVideoSliceError,
|
|
86
|
+
MultiVideoSlicePlan,
|
|
87
|
+
MultiVideoSliceRange,
|
|
88
|
+
MultiVideoStreamEligibility,
|
|
89
|
+
MultiVideoStreamSliceInfo,
|
|
90
|
+
MultiVideoToRangedSlicer,
|
|
91
|
+
)
|
|
92
|
+
except Exception as exc: # pragma: no cover - exercised in environments without av/pyavif
|
|
93
|
+
MultiVideoToRangedSlicer = _build_missing_class(
|
|
94
|
+
"MultiVideoToRangedSlicer", "pip install av pyavif", exc
|
|
95
|
+
)
|
|
96
|
+
MultiVideoSliceEligibility = _build_missing_class(
|
|
97
|
+
"MultiVideoSliceEligibility", "pip install av pyavif", exc
|
|
98
|
+
)
|
|
99
|
+
MultiVideoSliceEligibilityMetadata = _build_missing_class(
|
|
100
|
+
"MultiVideoSliceEligibilityMetadata", "pip install av pyavif", exc
|
|
101
|
+
)
|
|
102
|
+
MultiVideoSliceError = _build_missing_class(
|
|
103
|
+
"MultiVideoSliceError", "pip install av pyavif", exc
|
|
104
|
+
)
|
|
105
|
+
MultiVideoSlicePlan = _build_missing_class("MultiVideoSlicePlan", "pip install av pyavif", exc)
|
|
106
|
+
MultiVideoSliceRange = _build_missing_class(
|
|
107
|
+
"MultiVideoSliceRange", "pip install av pyavif", exc
|
|
108
|
+
)
|
|
109
|
+
MultiVideoStreamEligibility = _build_missing_class(
|
|
110
|
+
"MultiVideoStreamEligibility", "pip install av pyavif", exc
|
|
111
|
+
)
|
|
112
|
+
MultiVideoStreamSliceInfo = _build_missing_class(
|
|
113
|
+
"MultiVideoStreamSliceInfo", "pip install av pyavif", exc
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
__all__ = [
|
|
117
|
+
'Dataset',
|
|
118
|
+
'FrameReader',
|
|
119
|
+
'auto_detect_dataset',
|
|
120
|
+
'LegacyDataset',
|
|
121
|
+
'LegacyFrameReader',
|
|
122
|
+
'RangedDataset',
|
|
123
|
+
'RangedFrameReader',
|
|
124
|
+
'RangedMp4FrameReader',
|
|
125
|
+
'MultiVideoDataset',
|
|
126
|
+
'MultiVideoFrameReader',
|
|
127
|
+
'DatasetWriter',
|
|
128
|
+
'RangedDatasetWriter',
|
|
129
|
+
'RangedNvencSettings',
|
|
130
|
+
'LegacyDatasetWriter',
|
|
131
|
+
'MultiVideoDatasetWriter',
|
|
132
|
+
'MultiVideoToRangedSlicer',
|
|
133
|
+
'MultiVideoSliceEligibility',
|
|
134
|
+
'MultiVideoSliceEligibilityMetadata',
|
|
135
|
+
'MultiVideoSliceError',
|
|
136
|
+
'MultiVideoSlicePlan',
|
|
137
|
+
'MultiVideoSliceRange',
|
|
138
|
+
'MultiVideoStreamEligibility',
|
|
139
|
+
'MultiVideoStreamSliceInfo',
|
|
140
|
+
'calculate_psnr',
|
|
141
|
+
'NvdecRuntimeInfo',
|
|
142
|
+
'MultiVideoDecodeBenchmarkResult',
|
|
143
|
+
'inspect_nvdec_runtime',
|
|
144
|
+
'summarize_multivideo_dataset',
|
|
145
|
+
'benchmark_multivideo_decode',
|
|
146
|
+
'GpuDecodeMediaSupport',
|
|
147
|
+
'GpuDecodeSupportReport',
|
|
148
|
+
'gpu_decode_support',
|
|
149
|
+
'supports_gpu_decode',
|
|
150
|
+
'gpu_enabled',
|
|
151
|
+
'clear_gpu_override',
|
|
152
|
+
'gpu_disabled',
|
|
153
|
+
'gpu_enabled_override',
|
|
154
|
+
'gpu_usage_allowed',
|
|
155
|
+
'gpu_nvenc_disabled_globally',
|
|
156
|
+
'nvdec_decode_allowed',
|
|
157
|
+
'nvenc_encode_allowed',
|
|
158
|
+
'nvdec_decoding_usable',
|
|
159
|
+
'nvenc_encoding_usable',
|
|
160
|
+
'pynvvideocodec_importable',
|
|
161
|
+
'DatasetDownloader',
|
|
162
|
+
'RangedDatasetDownloader',
|
|
163
|
+
'LegacyDatasetDownloader',
|
|
164
|
+
'MultiVideoDatasetDownloader',
|
|
165
|
+
'infer_and_download_dataset',
|
|
166
|
+
'register_downloader',
|
|
167
|
+
'CloudStorageUrl',
|
|
168
|
+
'CloudStorageProviderType',
|
|
169
|
+
]
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Optional, Tuple
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CloudStorageProviderType(Enum):
|
|
9
|
+
AWS_S3 = 's3'
|
|
10
|
+
GOOGLE_STORAGE = 'gs'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CloudStorageUrl:
|
|
14
|
+
def __init__(self, url: str):
|
|
15
|
+
parsed = urlparse(url)
|
|
16
|
+
if parsed.scheme not in ("s3", "gs"):
|
|
17
|
+
raise ValueError(f"Invalid cloud URI scheme: {parsed.scheme}. Expected 's3' or 'gs'")
|
|
18
|
+
|
|
19
|
+
if not parsed.netloc:
|
|
20
|
+
raise ValueError(f"Invalid cloud URI: missing bucket name in {url}")
|
|
21
|
+
|
|
22
|
+
self.schema: str = parsed.scheme
|
|
23
|
+
self.bucket: str = parsed.netloc
|
|
24
|
+
path = parsed.path.lstrip("/")
|
|
25
|
+
if path and not path.endswith('/'):
|
|
26
|
+
path += "/"
|
|
27
|
+
self.path: str = path
|
|
28
|
+
self.cloud: CloudStorageProviderType = (
|
|
29
|
+
CloudStorageProviderType.AWS_S3
|
|
30
|
+
if parsed.scheme == "s3"
|
|
31
|
+
else CloudStorageProviderType.GOOGLE_STORAGE
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def from_parts(schema: str, bucket: str, path: str = "") -> "CloudStorageUrl":
|
|
36
|
+
path = path.lstrip("/")
|
|
37
|
+
return CloudStorageUrl(f"{schema}://{bucket}/{path}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BaseCloudStorageProvider(ABC):
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def list_objects(self, bucket: str, prefix: str, delimiter: Optional[str] = None) -> Tuple[List[str], List[str]]:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def download_file(self, bucket: str, key: str, local_path: Path) -> None:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AwsS3CloudStorageProvider(BaseCloudStorageProvider):
|
|
51
|
+
def __init__(self):
|
|
52
|
+
try:
|
|
53
|
+
import boto3
|
|
54
|
+
from botocore.exceptions import ClientError, NoCredentialsError
|
|
55
|
+
except ImportError as exc:
|
|
56
|
+
raise ImportError(
|
|
57
|
+
"boto3 is required for S3 support. "
|
|
58
|
+
"Install it with: pip install mvdata[s3]"
|
|
59
|
+
) from exc
|
|
60
|
+
|
|
61
|
+
self._ClientError = ClientError
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
self._client = boto3.client("s3")
|
|
65
|
+
except NoCredentialsError as exc:
|
|
66
|
+
raise RuntimeError(
|
|
67
|
+
"AWS credentials not found. Configure credentials using AWS CLI, "
|
|
68
|
+
"environment variables, or IAM role."
|
|
69
|
+
) from exc
|
|
70
|
+
|
|
71
|
+
def list_objects(self, bucket: str, prefix: str, delimiter: Optional[str] = None) -> Tuple[List[str], List[str]]:
|
|
72
|
+
objects = []
|
|
73
|
+
common_prefixes = []
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
paginator = self._client.get_paginator("list_objects_v2")
|
|
77
|
+
|
|
78
|
+
page_kwargs = {
|
|
79
|
+
"Bucket": bucket,
|
|
80
|
+
"Prefix": prefix,
|
|
81
|
+
}
|
|
82
|
+
if delimiter:
|
|
83
|
+
page_kwargs["Delimiter"] = delimiter
|
|
84
|
+
|
|
85
|
+
for page in paginator.paginate(**page_kwargs):
|
|
86
|
+
if "Contents" in page:
|
|
87
|
+
objects.extend([obj["Key"] for obj in page["Contents"]])
|
|
88
|
+
|
|
89
|
+
if delimiter and "CommonPrefixes" in page:
|
|
90
|
+
common_prefixes.extend([cp["Prefix"] for cp in page["CommonPrefixes"]])
|
|
91
|
+
|
|
92
|
+
return objects, common_prefixes
|
|
93
|
+
|
|
94
|
+
except self._ClientError as e:
|
|
95
|
+
raise RuntimeError(f"S3 access error: {e}") from e
|
|
96
|
+
|
|
97
|
+
def download_file(self, bucket: str, key: str, local_path: Path) -> None:
|
|
98
|
+
local_path.parent.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
self._client.download_file(bucket, key, str(local_path))
|
|
102
|
+
except self._ClientError as e:
|
|
103
|
+
raise RuntimeError(f"S3 download error for {key}: {e}") from e
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class GoogleStorageCloudStorageProvider(BaseCloudStorageProvider):
|
|
107
|
+
def __init__(self):
|
|
108
|
+
try:
|
|
109
|
+
from google.cloud import storage
|
|
110
|
+
except ImportError as exc:
|
|
111
|
+
raise ImportError(
|
|
112
|
+
"google-cloud-storage is required for GCS support. "
|
|
113
|
+
"Install it with: pip install mvdata[gcs]"
|
|
114
|
+
) from exc
|
|
115
|
+
|
|
116
|
+
from google.api_core.exceptions import Forbidden, NotFound, GoogleAPICallError # type: ignore[import-untyped]
|
|
117
|
+
from google.auth.exceptions import DefaultCredentialsError # type: ignore[import-untyped]
|
|
118
|
+
|
|
119
|
+
self._Forbidden = Forbidden
|
|
120
|
+
self._NotFound = NotFound
|
|
121
|
+
self._GoogleAPICallError = GoogleAPICallError
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
self._client = storage.Client()
|
|
125
|
+
except DefaultCredentialsError as exc:
|
|
126
|
+
raise RuntimeError(
|
|
127
|
+
"GCS credentials not found. Configure credentials using "
|
|
128
|
+
"GOOGLE_APPLICATION_CREDENTIALS environment variable, "
|
|
129
|
+
"gcloud auth, or service account."
|
|
130
|
+
) from exc
|
|
131
|
+
|
|
132
|
+
def list_objects(self, bucket: str, prefix: str, delimiter: Optional[str] = None) -> Tuple[List[str], List[str]]:
|
|
133
|
+
objects = []
|
|
134
|
+
common_prefixes = []
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
bucket_obj = self._client.bucket(bucket)
|
|
138
|
+
blobs = self._client.list_blobs(bucket_obj, prefix=prefix, delimiter=delimiter)
|
|
139
|
+
|
|
140
|
+
for blob in blobs:
|
|
141
|
+
objects.append(blob.name)
|
|
142
|
+
|
|
143
|
+
if delimiter:
|
|
144
|
+
common_prefixes = list(blobs.prefixes)
|
|
145
|
+
|
|
146
|
+
return objects, common_prefixes
|
|
147
|
+
|
|
148
|
+
except (self._Forbidden, self._NotFound) as e:
|
|
149
|
+
raise RuntimeError(f"GCS access error: {e}") from e
|
|
150
|
+
except self._GoogleAPICallError as e:
|
|
151
|
+
raise RuntimeError(f"GCS access error: {e}") from e
|
|
152
|
+
|
|
153
|
+
def download_file(self, bucket: str, key: str, local_path: Path) -> None:
|
|
154
|
+
local_path.parent.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
bucket_obj = self._client.bucket(bucket)
|
|
158
|
+
blob = bucket_obj.blob(key)
|
|
159
|
+
blob.download_to_filename(str(local_path))
|
|
160
|
+
except (self._Forbidden, self._NotFound) as e:
|
|
161
|
+
raise RuntimeError(f"GCS download error for {key}: {e}") from e
|
|
162
|
+
except self._GoogleAPICallError as e:
|
|
163
|
+
raise RuntimeError(f"GCS download error for {key}: {e}") from e
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class CloudStorageProvider:
|
|
167
|
+
_providers: dict[CloudStorageProviderType, BaseCloudStorageProvider] = {}
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def _get_provider(cls, provider_type: CloudStorageProviderType) -> BaseCloudStorageProvider:
|
|
171
|
+
if provider_type not in cls._providers:
|
|
172
|
+
if provider_type == CloudStorageProviderType.AWS_S3:
|
|
173
|
+
cls._providers[provider_type] = AwsS3CloudStorageProvider()
|
|
174
|
+
else:
|
|
175
|
+
cls._providers[provider_type] = GoogleStorageCloudStorageProvider()
|
|
176
|
+
return cls._providers[provider_type]
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def list_objects(cls, url: CloudStorageUrl, delimiter: Optional[str] = None) -> Tuple[List[str], List[str]]:
|
|
180
|
+
"""
|
|
181
|
+
List objects in cloud storage bucket with given prefix.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
url: CloudStorageUrl with schema, bucket, and path prefix
|
|
185
|
+
delimiter: Optional delimiter for hierarchical listing (e.g., '/')
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Tuple of (object_keys, common_prefixes)
|
|
189
|
+
"""
|
|
190
|
+
provider = cls._get_provider(url.cloud)
|
|
191
|
+
return provider.list_objects(url.bucket, url.path, delimiter)
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def download_file(cls, url: CloudStorageUrl, local_path: Path) -> None:
|
|
195
|
+
"""
|
|
196
|
+
Download a single file from cloud storage.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
url: CloudStorageUrl with schema, bucket, and path to the object
|
|
200
|
+
local_path: Local file path to save to
|
|
201
|
+
"""
|
|
202
|
+
key = url.path.rstrip("/")
|
|
203
|
+
provider = cls._get_provider(url.cloud)
|
|
204
|
+
provider.download_file(url.bucket, key, local_path)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""NVENC / NVDEC / PyAV codec helpers for mvdata.
|
|
2
|
+
|
|
3
|
+
This subpackage is split into small modules:
|
|
4
|
+
|
|
5
|
+
- :mod:`mvdata.codec._imports` – lazy optional-dep imports and constants
|
|
6
|
+
- :mod:`mvdata.codec.probe` – bit-depth probing and GPU capability checks
|
|
7
|
+
- :mod:`mvdata.codec.frames` – decoded-frame → HWC RGB (numpy and cupy)
|
|
8
|
+
- :mod:`mvdata.codec.encode` – RGB → NVENC elementary stream → MP4
|
|
9
|
+
- :mod:`mvdata.codec.decode` – NVDEC / PyAV decode of MP4
|
|
10
|
+
- :mod:`mvdata.codec.select` – round-trip PSNR and auto codec selection
|
|
11
|
+
|
|
12
|
+
The flat namespace here is preserved for backward compatibility with callers
|
|
13
|
+
that use ``from mvdata.codec import X`` or the top-level ``mvdata.nvenc_codec``
|
|
14
|
+
shim.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from ._imports import (
|
|
18
|
+
AUTO_NVENC_CHROMA_SUBSAMPLING,
|
|
19
|
+
AUTO_NVENC_CODEC_CANDIDATES,
|
|
20
|
+
AUTO_NVENC_MIN_PSNR_DB,
|
|
21
|
+
AUTO_NVENC_PRESET,
|
|
22
|
+
AUTO_NVENC_RANGE_MODE,
|
|
23
|
+
_PYNVC_API_REF,
|
|
24
|
+
normalize_codec_name,
|
|
25
|
+
try_import_av,
|
|
26
|
+
try_import_cupy,
|
|
27
|
+
try_import_pynvvideocodec,
|
|
28
|
+
try_import_torch,
|
|
29
|
+
)
|
|
30
|
+
from .decode import (
|
|
31
|
+
_try_open_nvdec,
|
|
32
|
+
decode_mp4_to_rgb,
|
|
33
|
+
decode_mp4_to_rgb_nvdec,
|
|
34
|
+
decode_mp4_to_rgb_pyav,
|
|
35
|
+
preferred_decoder_for_mp4,
|
|
36
|
+
)
|
|
37
|
+
from .encode import (
|
|
38
|
+
assert_yuv444_encode_supported,
|
|
39
|
+
best_quality_extra_kwargs,
|
|
40
|
+
create_nvenc_encoder,
|
|
41
|
+
encode_rgb_frames_to_elementary_stream,
|
|
42
|
+
encode_rgb_iter_to_elementary_stream,
|
|
43
|
+
encode_rgb_iter_to_mp4,
|
|
44
|
+
encode_rgb_sequence_to_mp4,
|
|
45
|
+
ffmpeg_executable,
|
|
46
|
+
mux_elementary_to_mp4,
|
|
47
|
+
pad_rgb_for_nvenc,
|
|
48
|
+
resolve_nvenc_bitrate_bps,
|
|
49
|
+
rgb_hwc_to_nvenc_bgra_hwc4,
|
|
50
|
+
rgb_hwc_to_yuv444_planar,
|
|
51
|
+
suggest_nvenc_vbr_bitrate_bps,
|
|
52
|
+
)
|
|
53
|
+
from .frames import (
|
|
54
|
+
_decoded_frame_to_rgb_cupy,
|
|
55
|
+
_decoded_frame_to_rgb_numpy,
|
|
56
|
+
_normalize_rgb_numpy_output,
|
|
57
|
+
_pyav_frame_to_rgb,
|
|
58
|
+
numpy_to_cupy_rgb,
|
|
59
|
+
)
|
|
60
|
+
from .probe import (
|
|
61
|
+
infer_video_bit_depth_from_frame,
|
|
62
|
+
infer_video_bit_depth_from_pixel_format_name,
|
|
63
|
+
infer_video_bit_depth_from_stream,
|
|
64
|
+
infer_video_bit_depth_from_video_format,
|
|
65
|
+
nvdec_decode_compatibility_issue,
|
|
66
|
+
nvdec_decode_compatibility_issue_for_path,
|
|
67
|
+
nvenc_encode_compatibility_issue,
|
|
68
|
+
nvenc_encode_compatibility_issue_for_rgb,
|
|
69
|
+
probe_video_bit_depth,
|
|
70
|
+
probe_video_bit_depth_pyav,
|
|
71
|
+
probe_video_stream_metadata,
|
|
72
|
+
pynvc_chroma_format,
|
|
73
|
+
pynvc_codec_enum,
|
|
74
|
+
safe_get_decoder_caps,
|
|
75
|
+
safe_get_encoder_caps,
|
|
76
|
+
)
|
|
77
|
+
from .select import roundtrip_nvenc_stream_psnr, select_auto_nvenc_candidate_for_rgb
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
"AUTO_NVENC_CHROMA_SUBSAMPLING",
|
|
81
|
+
"AUTO_NVENC_CODEC_CANDIDATES",
|
|
82
|
+
"AUTO_NVENC_MIN_PSNR_DB",
|
|
83
|
+
"AUTO_NVENC_PRESET",
|
|
84
|
+
"AUTO_NVENC_RANGE_MODE",
|
|
85
|
+
"assert_yuv444_encode_supported",
|
|
86
|
+
"best_quality_extra_kwargs",
|
|
87
|
+
"create_nvenc_encoder",
|
|
88
|
+
"decode_mp4_to_rgb",
|
|
89
|
+
"decode_mp4_to_rgb_nvdec",
|
|
90
|
+
"decode_mp4_to_rgb_pyav",
|
|
91
|
+
"encode_rgb_frames_to_elementary_stream",
|
|
92
|
+
"encode_rgb_iter_to_elementary_stream",
|
|
93
|
+
"encode_rgb_iter_to_mp4",
|
|
94
|
+
"encode_rgb_sequence_to_mp4",
|
|
95
|
+
"ffmpeg_executable",
|
|
96
|
+
"infer_video_bit_depth_from_frame",
|
|
97
|
+
"infer_video_bit_depth_from_pixel_format_name",
|
|
98
|
+
"infer_video_bit_depth_from_stream",
|
|
99
|
+
"infer_video_bit_depth_from_video_format",
|
|
100
|
+
"mux_elementary_to_mp4",
|
|
101
|
+
"normalize_codec_name",
|
|
102
|
+
"numpy_to_cupy_rgb",
|
|
103
|
+
"nvdec_decode_compatibility_issue",
|
|
104
|
+
"nvdec_decode_compatibility_issue_for_path",
|
|
105
|
+
"nvenc_encode_compatibility_issue",
|
|
106
|
+
"nvenc_encode_compatibility_issue_for_rgb",
|
|
107
|
+
"pad_rgb_for_nvenc",
|
|
108
|
+
"preferred_decoder_for_mp4",
|
|
109
|
+
"probe_video_bit_depth",
|
|
110
|
+
"probe_video_bit_depth_pyav",
|
|
111
|
+
"probe_video_stream_metadata",
|
|
112
|
+
"pynvc_chroma_format",
|
|
113
|
+
"pynvc_codec_enum",
|
|
114
|
+
"resolve_nvenc_bitrate_bps",
|
|
115
|
+
"rgb_hwc_to_nvenc_bgra_hwc4",
|
|
116
|
+
"rgb_hwc_to_yuv444_planar",
|
|
117
|
+
"roundtrip_nvenc_stream_psnr",
|
|
118
|
+
"safe_get_decoder_caps",
|
|
119
|
+
"safe_get_encoder_caps",
|
|
120
|
+
"select_auto_nvenc_candidate_for_rgb",
|
|
121
|
+
"suggest_nvenc_vbr_bitrate_bps",
|
|
122
|
+
"try_import_av",
|
|
123
|
+
"try_import_cupy",
|
|
124
|
+
"try_import_pynvvideocodec",
|
|
125
|
+
"try_import_torch",
|
|
126
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Lazy optional-dependency imports and codec name normalisation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
_PYNVC_API_REF = "PyNvVideoCodec_API_Reference.pdf"
|
|
9
|
+
|
|
10
|
+
AUTO_NVENC_CODEC_CANDIDATES = ("av1", "hevc", "h264")
|
|
11
|
+
AUTO_NVENC_CHROMA_SUBSAMPLING = "444"
|
|
12
|
+
AUTO_NVENC_PRESET = "P7"
|
|
13
|
+
AUTO_NVENC_RANGE_MODE = "full"
|
|
14
|
+
AUTO_NVENC_MIN_PSNR_DB = 38.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def try_import_pynvvideocodec() -> Any:
|
|
18
|
+
return importlib.import_module("PyNvVideoCodec")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _try_import(name: str) -> Any | None:
|
|
22
|
+
try:
|
|
23
|
+
return importlib.import_module(name)
|
|
24
|
+
except ImportError:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def try_import_av() -> Any | None:
|
|
29
|
+
return _try_import("av")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def try_import_torch() -> Any | None:
|
|
33
|
+
return _try_import("torch")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def try_import_cupy() -> Any | None:
|
|
37
|
+
return _try_import("cupy")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def normalize_codec_name(codec: str) -> str:
|
|
41
|
+
c = codec.strip().lower()
|
|
42
|
+
return "hevc" if c == "h265" else c
|