mvdata 0.9.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.
- mvdata/__init__.py +169 -0
- mvdata/cloud_storage.py +204 -0
- mvdata/codec/__init__.py +126 -0
- mvdata/codec/_imports.py +42 -0
- mvdata/codec/decode.py +123 -0
- mvdata/codec/encode.py +313 -0
- mvdata/codec/frames.py +199 -0
- mvdata/codec/probe.py +239 -0
- mvdata/codec/select.py +153 -0
- mvdata/dataset_base.py +1049 -0
- mvdata/downloader.py +806 -0
- mvdata/gpu_policy.py +128 -0
- mvdata/gpu_support.py +384 -0
- mvdata/image_metrics.py +24 -0
- mvdata/legacy_writer.py +371 -0
- mvdata/multivideo.py +982 -0
- mvdata/multivideo_decode_benchmark.py +216 -0
- mvdata/multivideo_slicer.py +930 -0
- mvdata/multivideo_writer.py +375 -0
- mvdata/nvdec_parallel.py +145 -0
- mvdata/nvenc_codec.py +17 -0
- mvdata/per_frame.py +1799 -0
- mvdata/ranged.py +1349 -0
- mvdata/ranged_writer.py +964 -0
- mvdata/stash_utils.py +358 -0
- mvdata/utils.py +33 -0
- mvdata/video_stream_reader.py +490 -0
- mvdata/write_progress.py +213 -0
- mvdata/writer_base.py +161 -0
- mvdata-0.9.0.dist-info/METADATA +52 -0
- mvdata-0.9.0.dist-info/RECORD +33 -0
- mvdata-0.9.0.dist-info/WHEEL +5 -0
- mvdata-0.9.0.dist-info/top_level.txt +1 -0
mvdata/__init__.py
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
|
+
]
|
mvdata/cloud_storage.py
ADDED
|
@@ -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)
|
mvdata/codec/__init__.py
ADDED
|
@@ -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
|
+
]
|
mvdata/codec/_imports.py
ADDED
|
@@ -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
|
mvdata/codec/decode.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""NVDEC / PyAV decode paths for MP4 files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, List
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ..gpu_policy import nvdec_decode_allowed
|
|
11
|
+
from ._imports import try_import_av, try_import_cupy, try_import_pynvvideocodec, try_import_torch
|
|
12
|
+
from .frames import _decoded_frame_to_rgb_numpy, _pyav_frame_to_rgb
|
|
13
|
+
from .probe import (
|
|
14
|
+
infer_video_bit_depth_from_stream,
|
|
15
|
+
nvdec_decode_compatibility_issue,
|
|
16
|
+
nvdec_decode_compatibility_issue_for_path,
|
|
17
|
+
probe_video_stream_metadata,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _try_open_nvdec(nvc, path_str: str, gpu_id: int, use_device_memory: bool):
|
|
22
|
+
"""Open a SimpleDecoder, tolerating API-version differences in the kwarg surface."""
|
|
23
|
+
base = dict(
|
|
24
|
+
gpu_id=gpu_id,
|
|
25
|
+
use_device_memory=use_device_memory,
|
|
26
|
+
output_color_type=nvc.OutputColorType.RGB,
|
|
27
|
+
)
|
|
28
|
+
for extra in ({}, {"need_scanned_stream_metadata": True}):
|
|
29
|
+
try:
|
|
30
|
+
return nvc.SimpleDecoder(path_str, **base, **extra)
|
|
31
|
+
except TypeError:
|
|
32
|
+
if extra:
|
|
33
|
+
try:
|
|
34
|
+
return nvc.SimpleDecoder(path_str, **base)
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def decode_mp4_to_rgb_pyav(mp4_path: Path, expect_count: int) -> List[np.ndarray]:
|
|
43
|
+
av = try_import_av()
|
|
44
|
+
if av is None:
|
|
45
|
+
raise RuntimeError("PyAV is required for CPU decode.")
|
|
46
|
+
out: List[np.ndarray] = []
|
|
47
|
+
with av.open(str(mp4_path)) as container:
|
|
48
|
+
stream = container.streams.video[0]
|
|
49
|
+
bit_depth = infer_video_bit_depth_from_stream(stream)
|
|
50
|
+
for frame in container.decode(stream):
|
|
51
|
+
out.append(_pyav_frame_to_rgb(frame, bit_depth=bit_depth))
|
|
52
|
+
if len(out) >= expect_count:
|
|
53
|
+
break
|
|
54
|
+
return out
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def decode_mp4_to_rgb_nvdec(nvc, mp4_path: Path, gpu_id: int, expect_count: int) -> List[np.ndarray]:
|
|
58
|
+
"""Decode MP4 to RGB uint8/uint16 HWC frames via NVDEC SimpleDecoder."""
|
|
59
|
+
if not nvdec_decode_allowed():
|
|
60
|
+
raise RuntimeError(
|
|
61
|
+
"NVDEC decode disabled by MVDATA_DISABLE_GPU / MVDATA_DISABLE_NVDEC_DECODE"
|
|
62
|
+
)
|
|
63
|
+
meta = probe_video_stream_metadata(nvc, mp4_path)
|
|
64
|
+
bit_depth = int(meta.get("bitdepth", 8))
|
|
65
|
+
issue = nvdec_decode_compatibility_issue(
|
|
66
|
+
nvc, meta["width"], meta["height"],
|
|
67
|
+
gpu_id=gpu_id, codec=meta["codec"],
|
|
68
|
+
chroma_subsampling=meta["chroma_subsampling"], bitdepth=bit_depth,
|
|
69
|
+
)
|
|
70
|
+
if issue is not None:
|
|
71
|
+
raise RuntimeError(f"NVDEC decode unsupported for {mp4_path}: {issue}")
|
|
72
|
+
|
|
73
|
+
torch_mod = try_import_torch()
|
|
74
|
+
has_dlpack = torch_mod is not None or try_import_cupy() is not None
|
|
75
|
+
path_str = str(mp4_path)
|
|
76
|
+
|
|
77
|
+
last_err: Exception | None = None
|
|
78
|
+
for use_dev in ([True, False] if has_dlpack else [False]):
|
|
79
|
+
dec = _try_open_nvdec(nvc, path_str, gpu_id, use_dev)
|
|
80
|
+
if dec is None:
|
|
81
|
+
continue
|
|
82
|
+
try:
|
|
83
|
+
n = min(len(dec), expect_count)
|
|
84
|
+
frames: List[np.ndarray] = []
|
|
85
|
+
for i in range(n):
|
|
86
|
+
raw = dec.get_batch_frames_by_index([i])[0]
|
|
87
|
+
frames.append(_decoded_frame_to_rgb_numpy(raw, torch_mod, bit_depth=bit_depth))
|
|
88
|
+
return frames
|
|
89
|
+
except Exception as e:
|
|
90
|
+
last_err = e
|
|
91
|
+
|
|
92
|
+
raise RuntimeError(
|
|
93
|
+
"NVDEC SimpleDecoder could not produce RGB numpy frames. "
|
|
94
|
+
f"Install PyTorch or CuPy for DLPack, or decode with PyAV. Last error: {last_err}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def decode_mp4_to_rgb(
|
|
99
|
+
mp4_path: Path,
|
|
100
|
+
expect_count: int,
|
|
101
|
+
*,
|
|
102
|
+
decoder: str = "pyav",
|
|
103
|
+
nvc: Any | None = None,
|
|
104
|
+
gpu_id: int = 0,
|
|
105
|
+
) -> List[np.ndarray]:
|
|
106
|
+
if decoder == "pyav":
|
|
107
|
+
return decode_mp4_to_rgb_pyav(mp4_path, expect_count)
|
|
108
|
+
if decoder == "nvidia":
|
|
109
|
+
if nvc is None:
|
|
110
|
+
nvc = try_import_pynvvideocodec()
|
|
111
|
+
return decode_mp4_to_rgb_nvdec(nvc, mp4_path, gpu_id, expect_count)
|
|
112
|
+
raise ValueError(f"Unknown decoder: {decoder}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def preferred_decoder_for_mp4(nvc, mp4_path: Path, *, gpu_id: int = 0) -> tuple[str, str | None]:
|
|
116
|
+
issue = nvdec_decode_compatibility_issue_for_path(nvc, mp4_path, gpu_id)
|
|
117
|
+
if issue is None:
|
|
118
|
+
return "nvidia", None
|
|
119
|
+
if try_import_av() is not None:
|
|
120
|
+
return "pyav", issue
|
|
121
|
+
raise RuntimeError(
|
|
122
|
+
f"NVDEC decode unsupported for {mp4_path}: {issue}. PyAV is not installed for CPU fallback."
|
|
123
|
+
)
|