megadetector 10.0.1__py3-none-any.whl → 10.0.3__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.

Potentially problematic release.


This version of megadetector might be problematic. Click here for more details.

@@ -0,0 +1,303 @@
1
+ """
2
+
3
+ extract_frames_from_video.py
4
+
5
+ Extracts frames from a source video or folder of videos and writes those frames to jpeg files.
6
+ For single videos, writes frame images to the destination folder. For folders of videos, creates
7
+ subfolders in the destination folder (one per video) and writes frame images to those subfolders.
8
+
9
+ """
10
+
11
+ #%% Constants and imports
12
+
13
+ import argparse
14
+ import inspect
15
+ import json
16
+ import os
17
+ import sys
18
+
19
+ from megadetector.detection.video_utils import \
20
+ video_to_frames, video_folder_to_frames, is_video_file
21
+
22
+
23
+ #%% Options class
24
+
25
+ class FrameExtractionOptions:
26
+ """
27
+ Parameters controlling the behavior of extract_frames().
28
+ """
29
+
30
+ def __init__(self):
31
+
32
+ #: Number of workers to use for parallel processing
33
+ self.n_workers = 1
34
+
35
+ #: Use threads for parallel processing
36
+ self.parallelize_with_threads = False
37
+
38
+ #: JPEG quality for extracted frames
39
+ self.quality = 80
40
+
41
+ #: Maximum width for extracted frames (defaults to None)
42
+ self.max_width = None
43
+
44
+ #: Enable additional debug output
45
+ self.verbose = False
46
+
47
+ #: Sample every Nth frame starting from the first frame; if this is None
48
+ #: or 1, every frame is extracted. If this is a negative value, it's interpreted
49
+ #: as a sampling rate in seconds, which is rounded to the nearest frame sampling
50
+ #: rate. Mutually exclusive with detector_output_file.
51
+ self.frame_sample = None
52
+
53
+ #: Path to MegaDetector .json output file. When specified, extracts frames
54
+ #: referenced in this file. Mutually exclusive with frame_sample. [source]
55
+ #: must be a folder when this is specified.
56
+ self.detector_output_file = None
57
+
58
+
59
+ #%% Core functions
60
+
61
+ def extract_frames(source, destination, options=None):
62
+ """
63
+ Extracts frames from a video or folder of videos.
64
+
65
+ Args:
66
+ source (str): path to a single video file or folder of videos
67
+ destination (str): folder to write frame images to (will be created if it doesn't exist)
68
+ options (FrameExtractionOptions, optional): parameters controlling frame extraction
69
+
70
+ Returns:
71
+ tuple: for single videos, returns (list of frame filenames, frame rate).
72
+ for folders, returns (list of lists of frame filenames, list of frame rates, list
73
+ of video filenames)
74
+ """
75
+
76
+ if options is None:
77
+ options = FrameExtractionOptions()
78
+
79
+ # Validate inputs
80
+ if not os.path.exists(source):
81
+ raise ValueError('Source path {} does not exist'.format(source))
82
+
83
+ if os.path.abspath(source) == os.path.abspath(destination):
84
+ raise ValueError('Source and destination cannot be the same')
85
+
86
+ # Create destination folder if it doesn't exist
87
+ os.makedirs(destination, exist_ok=True)
88
+
89
+ # Determine whether source is a file or folder
90
+ source_is_file = os.path.isfile(source)
91
+
92
+ if source_is_file:
93
+
94
+ # Validate that source is a video file
95
+ if not is_video_file(source):
96
+ raise ValueError('Source file {} is not a video file'.format(source))
97
+
98
+ # detector_output_file requires source to be a folder
99
+ if options.detector_output_file is not None:
100
+ raise ValueError('detector_output_file option requires source to be a folder, not a file')
101
+
102
+ # Extract frames from single video
103
+ return video_to_frames(input_video_file=source,
104
+ output_folder=destination,
105
+ overwrite=True,
106
+ every_n_frames=options.frame_sample,
107
+ verbose=options.verbose,
108
+ quality=options.quality,
109
+ max_width=options.max_width,
110
+ allow_empty_videos=True)
111
+
112
+ else:
113
+
114
+ frames_to_extract = None
115
+ relative_paths_to_process = None
116
+
117
+ # Handle detector output file
118
+ if options.detector_output_file is not None:
119
+ frames_to_extract, relative_paths_to_process = _parse_detector_output(
120
+ options.detector_output_file, source, options.verbose)
121
+ options.frame_sample = None
122
+
123
+ return video_folder_to_frames(input_folder=source,
124
+ output_folder_base=destination,
125
+ recursive=True,
126
+ overwrite=True,
127
+ n_threads=options.n_workers,
128
+ every_n_frames=options.frame_sample,
129
+ verbose=options.verbose,
130
+ parallelization_uses_threads=options.parallelize_with_threads,
131
+ quality=options.quality,
132
+ max_width=options.max_width,
133
+ frames_to_extract=frames_to_extract,
134
+ relative_paths_to_process=relative_paths_to_process,
135
+ allow_empty_videos=True)
136
+
137
+ # ...def extract_frames(...)
138
+
139
+
140
+ def _parse_detector_output(detector_output_file, source_folder, verbose=False):
141
+ """
142
+ Parses a MegaDetector .json output file and returns frame extraction information.
143
+
144
+ Args:
145
+ detector_output_file (str): path to MegaDetector .json output file
146
+ source_folder (str): folder containing the source videos
147
+ verbose (bool, optional): enable additional debug output
148
+
149
+ Returns:
150
+ tuple: (frames_to_extract_dict, relative_paths_to_process) where:
151
+ - frames_to_extract_dict maps relative video paths to lists of frame numbers
152
+ - relative_paths_to_process is a list of relative video paths to process
153
+ """
154
+
155
+ print('Parsing detector output file: {}'.format(detector_output_file))
156
+
157
+ # Load the detector results
158
+ with open(detector_output_file, 'r') as f:
159
+ detector_results = json.load(f)
160
+
161
+ if 'images' not in detector_results:
162
+ raise ValueError('Detector output file does not contain "images" field')
163
+
164
+ images = detector_results['images']
165
+ frames_to_extract_dict = {}
166
+ video_files_in_results = set()
167
+
168
+ for image_entry in images:
169
+
170
+ file_path = image_entry['file']
171
+
172
+ # Skip non-video files
173
+ if not is_video_file(file_path):
174
+ if verbose:
175
+ print('Skipping non-video file {}'.format(file_path))
176
+ continue
177
+
178
+ # Check whether video file exists in source folder
179
+ full_video_path = os.path.join(source_folder, file_path)
180
+ if not os.path.isfile(full_video_path):
181
+ print('Warning: video file {} not found in source folder, skipping'.format(file_path))
182
+ continue
183
+
184
+ video_files_in_results.add(file_path)
185
+
186
+ # Determine which frames to extract for this video
187
+ frames_for_this_video = []
188
+
189
+ if 'frames_processed' in image_entry:
190
+ # Use the frames_processed field if available
191
+ frames_for_this_video = image_entry['frames_processed']
192
+ if verbose:
193
+ print('Video {}: using frames_processed field with {} frames'.format(
194
+ file_path, len(frames_for_this_video)))
195
+ else:
196
+ # Extract frames from detections
197
+ if ('detections' in image_entry) and (image_entry['detections'] is not None):
198
+ frame_numbers = set()
199
+ for detection in image_entry['detections']:
200
+ if 'frame_number' in detection:
201
+ frame_numbers.add(detection['frame_number'])
202
+ frames_for_this_video = sorted(list(frame_numbers))
203
+ if verbose:
204
+ print('Video {}: extracted {} unique frame numbers from detections'.format(
205
+ file_path, len(frames_for_this_video)))
206
+
207
+ if len(frames_for_this_video) > 0:
208
+ frames_to_extract_dict[file_path] = frames_for_this_video
209
+
210
+ # ...for each image/video in this file
211
+
212
+ relative_paths_to_process = sorted(list(video_files_in_results))
213
+
214
+ print('Found {} videos with frames to extract'.format(len(frames_to_extract_dict)))
215
+
216
+ return frames_to_extract_dict, relative_paths_to_process
217
+
218
+ # ...def _parse_detector_output(...)
219
+
220
+
221
+ #%% Command-line driver
222
+
223
+ def _args_to_object(args, obj):
224
+ """
225
+ Copy all fields from a Namespace (i.e., the output from parse_args) to an object.
226
+ Skips fields starting with _. Does not check existence in the target object.
227
+ """
228
+
229
+ for n, v in inspect.getmembers(args):
230
+ if not n.startswith('_'):
231
+ setattr(obj, n, v)
232
+
233
+
234
+ def main():
235
+ """
236
+ Command-line driver for extract_frames_from_video
237
+ """
238
+
239
+ parser = argparse.ArgumentParser(
240
+ description='Extract frames from videos and save as JPEG files')
241
+
242
+ parser.add_argument('source', type=str,
243
+ help='Path to a single video file or folder containing videos')
244
+ parser.add_argument('destination', type=str,
245
+ help='Output folder for extracted frames (will be created if it does not exist)')
246
+
247
+ parser.add_argument('--n_workers', type=int, default=1,
248
+ help='Number of workers to use for parallel processing (default: %(default)s)')
249
+ parser.add_argument('--parallelize_with_threads', action='store_true',
250
+ help='Use threads for parallel processing (default: use processes)')
251
+ parser.add_argument('--quality', type=int, default=80,
252
+ help='JPEG quality for extracted frames (default: %(default)s)')
253
+ parser.add_argument('--max_width', type=int, default=None,
254
+ help='Maximum width for extracted frames (default: no resizing)')
255
+ parser.add_argument('--verbose', action='store_true',
256
+ help='Enable additional debug output')
257
+
258
+ # Mutually exclusive group for frame sampling options
259
+ frame_group = parser.add_mutually_exclusive_group()
260
+ frame_group.add_argument('--frame_sample', type=float, default=None,
261
+ help='Sample every Nth frame starting from the first frame; if this is None or 1, ' +
262
+ 'every frame is extracted. If this is a negative value, it\'s interpreted as a ' +
263
+ 'sampling rate in seconds, which is rounded to the nearest frame sampling rate')
264
+ frame_group.add_argument('--detector_output_file', type=str, default=None,
265
+ help='Path to MegaDetector .json output file. When specified, extracts frames ' +
266
+ 'referenced in this file. Source must be a folder when this is specified.')
267
+
268
+ if len(sys.argv[1:]) == 0:
269
+ parser.print_help()
270
+ parser.exit()
271
+
272
+ args = parser.parse_args()
273
+
274
+ # Convert to an options object
275
+ options = FrameExtractionOptions()
276
+ _args_to_object(args, options)
277
+
278
+ # Additional validation
279
+ if options.detector_output_file is not None:
280
+ if not os.path.isfile(options.detector_output_file):
281
+ print('Error: detector_output_file {} does not exist'.format(options.detector_output_file))
282
+ sys.exit(1)
283
+
284
+ try:
285
+ result = extract_frames(args.source, args.destination, options)
286
+
287
+ if os.path.isfile(args.source):
288
+ frame_filenames, frame_rate = result
289
+ print('Extracted {} frames from {} (frame rate: {:.2f} fps)'.format(
290
+ len(frame_filenames), args.source, frame_rate))
291
+ else:
292
+ frame_filenames_by_video, fs_by_video, video_filenames = result
293
+ total_frames = sum(len(frames) for frames in frame_filenames_by_video)
294
+ print('Processed {} videos, extracted {} total frames'.format(
295
+ len(video_filenames), total_frames))
296
+
297
+ except Exception as e:
298
+ print('Error: {}'.format(str(e)))
299
+ sys.exit(1)
300
+
301
+
302
+ if __name__ == '__main__':
303
+ main()