megadetector 5.0.11__py3-none-any.whl → 5.0.13__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.
- megadetector/api/__init__.py +0 -0
- megadetector/api/batch_processing/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/batch_service/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/batch_service/score.py +439 -0
- megadetector/api/batch_processing/api_core/server.py +294 -0
- megadetector/api/batch_processing/api_core/server_api_config.py +97 -0
- megadetector/api/batch_processing/api_core/server_app_config.py +55 -0
- megadetector/api/batch_processing/api_core/server_batch_job_manager.py +220 -0
- megadetector/api/batch_processing/api_core/server_job_status_table.py +149 -0
- megadetector/api/batch_processing/api_core/server_orchestration.py +360 -0
- megadetector/api/batch_processing/api_core/server_utils.py +88 -0
- megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
- megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +46 -0
- megadetector/api/batch_processing/api_support/__init__.py +0 -0
- megadetector/api/batch_processing/api_support/summarize_daily_activity.py +152 -0
- megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
- megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
- megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +125 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -0
- megadetector/api/synchronous/__init__.py +0 -0
- megadetector/api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
- megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +152 -0
- megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +263 -0
- megadetector/api/synchronous/api_core/animal_detection_api/config.py +35 -0
- megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
- megadetector/api/synchronous/api_core/tests/load_test.py +110 -0
- megadetector/classification/__init__.py +0 -0
- megadetector/classification/aggregate_classifier_probs.py +108 -0
- megadetector/classification/analyze_failed_images.py +227 -0
- megadetector/classification/cache_batchapi_outputs.py +198 -0
- megadetector/classification/create_classification_dataset.py +627 -0
- megadetector/classification/crop_detections.py +516 -0
- megadetector/classification/csv_to_json.py +226 -0
- megadetector/classification/detect_and_crop.py +855 -0
- megadetector/classification/efficientnet/__init__.py +9 -0
- megadetector/classification/efficientnet/model.py +415 -0
- megadetector/classification/efficientnet/utils.py +607 -0
- megadetector/classification/evaluate_model.py +520 -0
- megadetector/classification/identify_mislabeled_candidates.py +152 -0
- megadetector/classification/json_to_azcopy_list.py +63 -0
- megadetector/classification/json_validator.py +699 -0
- megadetector/classification/map_classification_categories.py +276 -0
- megadetector/classification/merge_classification_detection_output.py +506 -0
- megadetector/classification/prepare_classification_script.py +194 -0
- megadetector/classification/prepare_classification_script_mc.py +228 -0
- megadetector/classification/run_classifier.py +287 -0
- megadetector/classification/save_mislabeled.py +110 -0
- megadetector/classification/train_classifier.py +827 -0
- megadetector/classification/train_classifier_tf.py +725 -0
- megadetector/classification/train_utils.py +323 -0
- megadetector/data_management/__init__.py +0 -0
- megadetector/data_management/annotations/__init__.py +0 -0
- megadetector/data_management/annotations/annotation_constants.py +34 -0
- megadetector/data_management/camtrap_dp_to_coco.py +237 -0
- megadetector/data_management/cct_json_utils.py +404 -0
- megadetector/data_management/cct_to_md.py +176 -0
- megadetector/data_management/cct_to_wi.py +289 -0
- megadetector/data_management/coco_to_labelme.py +283 -0
- megadetector/data_management/coco_to_yolo.py +662 -0
- megadetector/data_management/databases/__init__.py +0 -0
- megadetector/data_management/databases/add_width_and_height_to_db.py +33 -0
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +206 -0
- megadetector/data_management/databases/integrity_check_json_db.py +493 -0
- megadetector/data_management/databases/subset_json_db.py +115 -0
- megadetector/data_management/generate_crops_from_cct.py +149 -0
- megadetector/data_management/get_image_sizes.py +189 -0
- megadetector/data_management/importers/add_nacti_sizes.py +52 -0
- megadetector/data_management/importers/add_timestamps_to_icct.py +79 -0
- megadetector/data_management/importers/animl_results_to_md_results.py +158 -0
- megadetector/data_management/importers/auckland_doc_test_to_json.py +373 -0
- megadetector/data_management/importers/auckland_doc_to_json.py +201 -0
- megadetector/data_management/importers/awc_to_json.py +191 -0
- megadetector/data_management/importers/bellevue_to_json.py +273 -0
- megadetector/data_management/importers/cacophony-thermal-importer.py +793 -0
- megadetector/data_management/importers/carrizo_shrubfree_2018.py +269 -0
- megadetector/data_management/importers/carrizo_trail_cam_2017.py +289 -0
- megadetector/data_management/importers/cct_field_adjustments.py +58 -0
- megadetector/data_management/importers/channel_islands_to_cct.py +913 -0
- megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +180 -0
- megadetector/data_management/importers/eMammal/eMammal_helpers.py +249 -0
- megadetector/data_management/importers/eMammal/make_eMammal_json.py +223 -0
- megadetector/data_management/importers/ena24_to_json.py +276 -0
- megadetector/data_management/importers/filenames_to_json.py +386 -0
- megadetector/data_management/importers/helena_to_cct.py +283 -0
- megadetector/data_management/importers/idaho-camera-traps.py +1407 -0
- megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +294 -0
- megadetector/data_management/importers/jb_csv_to_json.py +150 -0
- megadetector/data_management/importers/mcgill_to_json.py +250 -0
- megadetector/data_management/importers/missouri_to_json.py +490 -0
- megadetector/data_management/importers/nacti_fieldname_adjustments.py +79 -0
- megadetector/data_management/importers/noaa_seals_2019.py +181 -0
- megadetector/data_management/importers/pc_to_json.py +365 -0
- megadetector/data_management/importers/plot_wni_giraffes.py +123 -0
- megadetector/data_management/importers/prepare-noaa-fish-data-for-lila.py +359 -0
- megadetector/data_management/importers/prepare_zsl_imerit.py +131 -0
- megadetector/data_management/importers/rspb_to_json.py +356 -0
- megadetector/data_management/importers/save_the_elephants_survey_A.py +320 -0
- megadetector/data_management/importers/save_the_elephants_survey_B.py +329 -0
- megadetector/data_management/importers/snapshot_safari_importer.py +758 -0
- megadetector/data_management/importers/snapshot_safari_importer_reprise.py +665 -0
- megadetector/data_management/importers/snapshot_serengeti_lila.py +1067 -0
- megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +150 -0
- megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +153 -0
- megadetector/data_management/importers/sulross_get_exif.py +65 -0
- megadetector/data_management/importers/timelapse_csv_set_to_json.py +490 -0
- megadetector/data_management/importers/ubc_to_json.py +399 -0
- megadetector/data_management/importers/umn_to_json.py +507 -0
- megadetector/data_management/importers/wellington_to_json.py +263 -0
- megadetector/data_management/importers/wi_to_json.py +442 -0
- megadetector/data_management/importers/zamba_results_to_md_results.py +181 -0
- megadetector/data_management/labelme_to_coco.py +547 -0
- megadetector/data_management/labelme_to_yolo.py +272 -0
- megadetector/data_management/lila/__init__.py +0 -0
- megadetector/data_management/lila/add_locations_to_island_camera_traps.py +97 -0
- megadetector/data_management/lila/add_locations_to_nacti.py +147 -0
- megadetector/data_management/lila/create_lila_blank_set.py +558 -0
- megadetector/data_management/lila/create_lila_test_set.py +152 -0
- megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
- megadetector/data_management/lila/download_lila_subset.py +178 -0
- megadetector/data_management/lila/generate_lila_per_image_labels.py +516 -0
- megadetector/data_management/lila/get_lila_annotation_counts.py +170 -0
- megadetector/data_management/lila/get_lila_image_counts.py +112 -0
- megadetector/data_management/lila/lila_common.py +300 -0
- megadetector/data_management/lila/test_lila_metadata_urls.py +132 -0
- megadetector/data_management/ocr_tools.py +870 -0
- megadetector/data_management/read_exif.py +809 -0
- megadetector/data_management/remap_coco_categories.py +84 -0
- megadetector/data_management/remove_exif.py +66 -0
- megadetector/data_management/rename_images.py +187 -0
- megadetector/data_management/resize_coco_dataset.py +189 -0
- megadetector/data_management/wi_download_csv_to_coco.py +247 -0
- megadetector/data_management/yolo_output_to_md_output.py +446 -0
- megadetector/data_management/yolo_to_coco.py +676 -0
- megadetector/detection/__init__.py +0 -0
- megadetector/detection/detector_training/__init__.py +0 -0
- megadetector/detection/detector_training/model_main_tf2.py +114 -0
- megadetector/detection/process_video.py +846 -0
- megadetector/detection/pytorch_detector.py +355 -0
- megadetector/detection/run_detector.py +779 -0
- megadetector/detection/run_detector_batch.py +1219 -0
- megadetector/detection/run_inference_with_yolov5_val.py +1087 -0
- megadetector/detection/run_tiled_inference.py +934 -0
- megadetector/detection/tf_detector.py +192 -0
- megadetector/detection/video_utils.py +698 -0
- megadetector/postprocessing/__init__.py +0 -0
- megadetector/postprocessing/add_max_conf.py +64 -0
- megadetector/postprocessing/categorize_detections_by_size.py +165 -0
- megadetector/postprocessing/classification_postprocessing.py +716 -0
- megadetector/postprocessing/combine_api_outputs.py +249 -0
- megadetector/postprocessing/compare_batch_results.py +966 -0
- megadetector/postprocessing/convert_output_format.py +396 -0
- megadetector/postprocessing/load_api_results.py +195 -0
- megadetector/postprocessing/md_to_coco.py +310 -0
- megadetector/postprocessing/md_to_labelme.py +330 -0
- megadetector/postprocessing/merge_detections.py +412 -0
- megadetector/postprocessing/postprocess_batch_results.py +1908 -0
- megadetector/postprocessing/remap_detection_categories.py +170 -0
- megadetector/postprocessing/render_detection_confusion_matrix.py +660 -0
- megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +211 -0
- megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +83 -0
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1635 -0
- megadetector/postprocessing/separate_detections_into_folders.py +730 -0
- megadetector/postprocessing/subset_json_detector_output.py +700 -0
- megadetector/postprocessing/top_folders_to_bottom.py +223 -0
- megadetector/taxonomy_mapping/__init__.py +0 -0
- megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +150 -0
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +142 -0
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +588 -0
- megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
- megadetector/taxonomy_mapping/simple_image_download.py +219 -0
- megadetector/taxonomy_mapping/species_lookup.py +834 -0
- megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
- megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
- megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
- megadetector/utils/__init__.py +0 -0
- megadetector/utils/azure_utils.py +178 -0
- megadetector/utils/ct_utils.py +613 -0
- megadetector/utils/directory_listing.py +246 -0
- megadetector/utils/md_tests.py +1164 -0
- megadetector/utils/path_utils.py +1045 -0
- megadetector/utils/process_utils.py +160 -0
- megadetector/utils/sas_blob_utils.py +509 -0
- megadetector/utils/split_locations_into_train_val.py +228 -0
- megadetector/utils/string_utils.py +92 -0
- megadetector/utils/url_utils.py +323 -0
- megadetector/utils/write_html_image_list.py +225 -0
- megadetector/visualization/__init__.py +0 -0
- megadetector/visualization/plot_utils.py +293 -0
- megadetector/visualization/render_images_with_thumbnails.py +275 -0
- megadetector/visualization/visualization_utils.py +1536 -0
- megadetector/visualization/visualize_db.py +552 -0
- megadetector/visualization/visualize_detector_output.py +405 -0
- {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/LICENSE +0 -0
- {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/METADATA +2 -2
- megadetector-5.0.13.dist-info/RECORD +201 -0
- megadetector-5.0.13.dist-info/top_level.txt +1 -0
- megadetector-5.0.11.dist-info/RECORD +0 -5
- megadetector-5.0.11.dist-info/top_level.txt +0 -1
- {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
process_video.py
|
|
4
|
+
|
|
5
|
+
Splits a video (or folder of videos) into frames, runs the frames through run_detector_batch.py,
|
|
6
|
+
and optionally stitches together results into a new video with detection boxes.
|
|
7
|
+
|
|
8
|
+
Operates by separating the video into frames, typically sampling every Nth frame, and writing
|
|
9
|
+
those frames to disk, before running MD. This approach clearly has a downside: it requires
|
|
10
|
+
a bunch more disk space, compared to extracting frames and running MD on them without ever
|
|
11
|
+
writing them to disk. The upside, though, is that this approach allows you to run repeat
|
|
12
|
+
detection elimination after running MegaDetector, and it allows allows more efficient re-use
|
|
13
|
+
of frames if you end up running MD more than once, or running multiple versions of MD.
|
|
14
|
+
|
|
15
|
+
TODO: optionally skip writing frames to disk, and process frames in memory.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
#%% Imports
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import tempfile
|
|
24
|
+
import argparse
|
|
25
|
+
import itertools
|
|
26
|
+
import json
|
|
27
|
+
import shutil
|
|
28
|
+
import getpass
|
|
29
|
+
|
|
30
|
+
from uuid import uuid1
|
|
31
|
+
|
|
32
|
+
from megadetector.detection import run_detector_batch
|
|
33
|
+
from megadetector.visualization import visualize_detector_output
|
|
34
|
+
from megadetector.utils.ct_utils import args_to_object
|
|
35
|
+
from megadetector.utils.path_utils import insert_before_extension, clean_path
|
|
36
|
+
from megadetector.detection.video_utils import video_to_frames
|
|
37
|
+
from megadetector.detection.video_utils import frames_to_video
|
|
38
|
+
from megadetector.detection.video_utils import frame_results_to_video_results
|
|
39
|
+
from megadetector.detection.video_utils import video_folder_to_frames
|
|
40
|
+
from megadetector.detection.video_utils import default_fourcc
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
#%% Classes
|
|
44
|
+
|
|
45
|
+
class ProcessVideoOptions:
|
|
46
|
+
"""
|
|
47
|
+
Options controlling the behavior of process_video()
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self):
|
|
51
|
+
|
|
52
|
+
#: Can be a model filename (.pt or .pb) or a model name (e.g. "MDV5A")
|
|
53
|
+
self.model_file = 'MDV5A'
|
|
54
|
+
|
|
55
|
+
#: Video (of folder of videos) to process
|
|
56
|
+
self.input_video_file = ''
|
|
57
|
+
|
|
58
|
+
#: .json file to which we should write results
|
|
59
|
+
self.output_json_file = None
|
|
60
|
+
|
|
61
|
+
#: File to which we should write a video with boxes, only relevant if
|
|
62
|
+
#: render_output_video is True
|
|
63
|
+
self.output_video_file = None
|
|
64
|
+
|
|
65
|
+
#: Folder to use for extracted frames; will use a folder in system temp space
|
|
66
|
+
#: if this is None
|
|
67
|
+
self.frame_folder = None
|
|
68
|
+
|
|
69
|
+
# Folder to use for rendered frames (if rendering output video); will use a folder
|
|
70
|
+
#: in system temp space if this is None
|
|
71
|
+
self.frame_rendering_folder = None
|
|
72
|
+
|
|
73
|
+
#: Should we render a video with detection boxes?
|
|
74
|
+
#:
|
|
75
|
+
#: Only supported when processing a single video, not a folder.
|
|
76
|
+
self.render_output_video = False
|
|
77
|
+
|
|
78
|
+
#: If we are rendering boxes to a new video, should we keep the temporary
|
|
79
|
+
#: rendered frames?
|
|
80
|
+
self.keep_rendered_frames = False
|
|
81
|
+
|
|
82
|
+
#: Should we keep the extracted frames?
|
|
83
|
+
self.keep_extracted_frames = False
|
|
84
|
+
|
|
85
|
+
#: Should we delete the entire folder the extracted frames are written to?
|
|
86
|
+
#:
|
|
87
|
+
#: By default, we delete the frame files but leave the (probably-empty) folder in place,
|
|
88
|
+
#: for no reason other than being paranoid about deleting folders.
|
|
89
|
+
self.force_extracted_frame_folder_deletion = False
|
|
90
|
+
|
|
91
|
+
#: Should we delete the entire folder the rendered frames are written to?
|
|
92
|
+
#:
|
|
93
|
+
#: By default, we delete the frame files but leave the (probably-empty) folder in place,
|
|
94
|
+
#: for no reason other than being paranoid about deleting folders.
|
|
95
|
+
self.force_rendered_frame_folder_deletion = False
|
|
96
|
+
|
|
97
|
+
#: If we've already run MegaDetector on this video or folder of videos, i.e. if we
|
|
98
|
+
#: find a corresponding MD results file, should we re-use it? Defaults to reprocessing.
|
|
99
|
+
self.reuse_results_if_available = False
|
|
100
|
+
|
|
101
|
+
#: If we've already split this video or folder of videos into frames, should we
|
|
102
|
+
#: we re-use those extracted frames? Defaults to reprocessing.
|
|
103
|
+
self.reuse_frames_if_available = False
|
|
104
|
+
|
|
105
|
+
#: If [input_video_file] is a folder, should we search for videos recursively?
|
|
106
|
+
self.recursive = False
|
|
107
|
+
|
|
108
|
+
#: Enable additional debug console output
|
|
109
|
+
self.verbose = False
|
|
110
|
+
|
|
111
|
+
#: fourcc code to use for writing videos; only relevant if render_output_video is True
|
|
112
|
+
self.fourcc = None
|
|
113
|
+
|
|
114
|
+
#: Confidence threshold to use for writing videos with boxes, only relevant if
|
|
115
|
+
#: if render_output_video is True. Defaults to choosing a reasonable threshold
|
|
116
|
+
#: based on the model version.
|
|
117
|
+
self.rendering_confidence_threshold = None
|
|
118
|
+
|
|
119
|
+
#: Detections below this threshold will not be included in the output file.
|
|
120
|
+
self.json_confidence_threshold = 0.005
|
|
121
|
+
|
|
122
|
+
#: Sample every Nth frame; set to None (default) or 1 to sample every frame. Typically
|
|
123
|
+
#: we sample down to around 3 fps, so for typical 30 fps videos, frame_sample=10 is a
|
|
124
|
+
#: typical value.
|
|
125
|
+
self.frame_sample = None
|
|
126
|
+
|
|
127
|
+
#: Number of workers to use for parallelization; set to <= 1 to disable parallelization
|
|
128
|
+
self.n_cores = 1
|
|
129
|
+
|
|
130
|
+
#: For debugging only, stop processing after a certain number of frames.
|
|
131
|
+
self.debug_max_frames = -1
|
|
132
|
+
|
|
133
|
+
#: File containing non-standard categories, typically only used if you're running a non-MD
|
|
134
|
+
#: detector.
|
|
135
|
+
self.class_mapping_filename = None
|
|
136
|
+
|
|
137
|
+
#: JPEG quality for frame output, from 0-100. Defaults to the opencv default (typically 95)
|
|
138
|
+
self.quality = 90
|
|
139
|
+
|
|
140
|
+
#: Resize frames so they're at most this wide
|
|
141
|
+
self.max_width = 1600
|
|
142
|
+
|
|
143
|
+
# ...class ProcessVideoOptions
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
#%% Functions
|
|
147
|
+
|
|
148
|
+
def _select_temporary_output_folders(options):
|
|
149
|
+
"""
|
|
150
|
+
Choose folders in system temp space for writing temporary frames. Does not create folders,
|
|
151
|
+
just defines them.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
tempdir = os.path.join(tempfile.gettempdir(), 'process_camera_trap_video')
|
|
155
|
+
|
|
156
|
+
# If we create a folder like "process_camera_trap_video" in the system temp dir, it may
|
|
157
|
+
# be the case that no one else can write to it, even to create user-specific subfolders.
|
|
158
|
+
# If we create a uuid-named folder in the system temp dir, we make a mess.
|
|
159
|
+
#
|
|
160
|
+
# Compromise with "process_camera_trap_video-[user]".
|
|
161
|
+
user_tempdir = tempdir + '-' + getpass.getuser()
|
|
162
|
+
|
|
163
|
+
# I don't know whether it's possible for a username to contain characters that are
|
|
164
|
+
# not valid filename characters, but just to be sure...
|
|
165
|
+
user_tempdir = clean_path(user_tempdir)
|
|
166
|
+
|
|
167
|
+
frame_output_folder = os.path.join(
|
|
168
|
+
user_tempdir, os.path.basename(options.input_video_file) + '_frames_' + str(uuid1()))
|
|
169
|
+
|
|
170
|
+
rendering_output_folder = os.path.join(
|
|
171
|
+
tempdir, os.path.basename(options.input_video_file) + '_detections_' + str(uuid1()))
|
|
172
|
+
|
|
173
|
+
temporary_folder_info = \
|
|
174
|
+
{
|
|
175
|
+
'temp_folder_base':user_tempdir,
|
|
176
|
+
'frame_output_folder':frame_output_folder,
|
|
177
|
+
'rendering_output_folder':rendering_output_folder
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return temporary_folder_info
|
|
181
|
+
|
|
182
|
+
# ...def _create_frame_output_folders(...)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _clean_up_rendered_frames(options,rendering_output_folder,detected_frame_files):
|
|
186
|
+
"""
|
|
187
|
+
If necessary, delete rendered frames and/or the entire rendering output folder.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
|
|
191
|
+
|
|
192
|
+
# (Optionally) delete the temporary directory we used for rendered detection images
|
|
193
|
+
if not options.keep_rendered_frames:
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
|
|
197
|
+
# If (a) we're supposed to delete the temporary rendering folder no
|
|
198
|
+
# matter where it is and (b) we created it in temp space, delete the
|
|
199
|
+
# whole tree
|
|
200
|
+
if options.force_rendered_frame_folder_deletion and \
|
|
201
|
+
(not caller_provided_rendering_output_folder):
|
|
202
|
+
|
|
203
|
+
if options.verbose:
|
|
204
|
+
print('Recursively deleting rendered frame folder {}'.format(
|
|
205
|
+
rendering_output_folder))
|
|
206
|
+
|
|
207
|
+
shutil.rmtree(rendering_output_folder)
|
|
208
|
+
|
|
209
|
+
# ...otherwise just delete the frames, but leave the folder in place
|
|
210
|
+
else:
|
|
211
|
+
|
|
212
|
+
if options.force_rendered_frame_folder_deletion:
|
|
213
|
+
assert caller_provided_rendering_output_folder
|
|
214
|
+
print('Warning: force_rendered_frame_folder_deletion supplied with a ' + \
|
|
215
|
+
'user-provided folder, only removing frames')
|
|
216
|
+
|
|
217
|
+
for rendered_frame_fn in detected_frame_files:
|
|
218
|
+
os.remove(rendered_frame_fn)
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
|
|
222
|
+
rendering_output_folder,str(e)))
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
elif options.force_rendered_frame_folder_deletion:
|
|
226
|
+
|
|
227
|
+
print('Warning: keep_rendered_frames and force_rendered_frame_folder_deletion both ' + \
|
|
228
|
+
'specified, not deleting')
|
|
229
|
+
|
|
230
|
+
# ...def _clean_up_rendered_frames(...)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _clean_up_extracted_frames(options,frame_output_folder,frame_filenames):
|
|
234
|
+
"""
|
|
235
|
+
If necessary, delete extracted frames and/or the entire temporary frame folder.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
caller_provided_frame_output_folder = (options.frame_folder is not None)
|
|
239
|
+
|
|
240
|
+
if not options.keep_extracted_frames:
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
|
|
244
|
+
# If (a) we're supposed to delete the temporary frame folder no
|
|
245
|
+
# matter where it is and (b) we created it in temp space, delete the
|
|
246
|
+
# whole tree.
|
|
247
|
+
if options.force_extracted_frame_folder_deletion and \
|
|
248
|
+
(not caller_provided_frame_output_folder):
|
|
249
|
+
|
|
250
|
+
if options.verbose:
|
|
251
|
+
print('Recursively deleting frame output folder {}'.format(frame_output_folder))
|
|
252
|
+
|
|
253
|
+
shutil.rmtree(frame_output_folder)
|
|
254
|
+
|
|
255
|
+
# ...otherwise just delete the frames, but leave the folder in place
|
|
256
|
+
else:
|
|
257
|
+
|
|
258
|
+
if options.force_extracted_frame_folder_deletion:
|
|
259
|
+
assert caller_provided_frame_output_folder
|
|
260
|
+
print('Warning: force_extracted_frame_folder_deletion supplied with a ' + \
|
|
261
|
+
'user-provided folder, only removing frames')
|
|
262
|
+
|
|
263
|
+
for extracted_frame_fn in frame_filenames:
|
|
264
|
+
os.remove(extracted_frame_fn)
|
|
265
|
+
|
|
266
|
+
except Exception as e:
|
|
267
|
+
print('Warning: error removing extracted frames from folder {}:\n{}'.format(
|
|
268
|
+
frame_output_folder,str(e)))
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
elif options.force_extracted_frame_folder_deletion:
|
|
272
|
+
|
|
273
|
+
print('Warning: keep_extracted_frames and force_extracted_frame_folder_deletion both ' + \
|
|
274
|
+
'specified, not deleting')
|
|
275
|
+
|
|
276
|
+
# ...def _clean_up_extracted_frames
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def process_video(options):
|
|
280
|
+
"""
|
|
281
|
+
Process a single video through MD, optionally writing a new video with boxes
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
options (ProcessVideoOptions): all the parameters used to control this process,
|
|
285
|
+
including filenames; see ProcessVideoOptions for details
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
dict: frame-level MegaDetector results, identical to what's in the output .json file
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
if options.output_json_file is None:
|
|
292
|
+
options.output_json_file = options.input_video_file + '.json'
|
|
293
|
+
|
|
294
|
+
if options.render_output_video and (options.output_video_file is None):
|
|
295
|
+
options.output_video_file = options.input_video_file + '.detections.mp4'
|
|
296
|
+
|
|
297
|
+
# Track whether frame and rendering folders were created by this script
|
|
298
|
+
caller_provided_frame_output_folder = (options.frame_folder is not None)
|
|
299
|
+
caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
|
|
300
|
+
|
|
301
|
+
# This does not create any folders, just defines temporary folder names in
|
|
302
|
+
# case we need them.
|
|
303
|
+
temporary_folder_info = _select_temporary_output_folders(options)
|
|
304
|
+
|
|
305
|
+
if (caller_provided_frame_output_folder):
|
|
306
|
+
frame_output_folder = options.frame_folder
|
|
307
|
+
else:
|
|
308
|
+
frame_output_folder = temporary_folder_info['frame_output_folder']
|
|
309
|
+
|
|
310
|
+
os.makedirs(frame_output_folder, exist_ok=True)
|
|
311
|
+
|
|
312
|
+
frame_filenames, Fs = video_to_frames(
|
|
313
|
+
options.input_video_file, frame_output_folder,
|
|
314
|
+
every_n_frames=options.frame_sample, overwrite=(not options.reuse_frames_if_available),
|
|
315
|
+
quality=options.quality, max_width=options.max_width, verbose=options.verbose)
|
|
316
|
+
|
|
317
|
+
image_file_names = frame_filenames
|
|
318
|
+
if options.debug_max_frames > 0:
|
|
319
|
+
image_file_names = image_file_names[0:options.debug_max_frames]
|
|
320
|
+
|
|
321
|
+
if options.reuse_results_if_available and \
|
|
322
|
+
os.path.isfile(options.output_json_file):
|
|
323
|
+
print('Loading results from {}'.format(options.output_json_file))
|
|
324
|
+
with open(options.output_json_file,'r') as f:
|
|
325
|
+
results = json.load(f)
|
|
326
|
+
else:
|
|
327
|
+
results = run_detector_batch.load_and_run_detector_batch(
|
|
328
|
+
options.model_file, image_file_names,
|
|
329
|
+
confidence_threshold=options.json_confidence_threshold,
|
|
330
|
+
n_cores=options.n_cores,
|
|
331
|
+
class_mapping_filename=options.class_mapping_filename,
|
|
332
|
+
quiet=True)
|
|
333
|
+
|
|
334
|
+
run_detector_batch.write_results_to_file(
|
|
335
|
+
results, options.output_json_file,
|
|
336
|
+
relative_path_base=frame_output_folder,
|
|
337
|
+
detector_file=options.model_file,
|
|
338
|
+
custom_metadata={'video_frame_rate':Fs})
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
## (Optionally) render output video
|
|
342
|
+
|
|
343
|
+
if options.render_output_video:
|
|
344
|
+
|
|
345
|
+
# Render detections to images
|
|
346
|
+
if (caller_provided_rendering_output_folder):
|
|
347
|
+
rendering_output_dir = options.frame_rendering_folder
|
|
348
|
+
else:
|
|
349
|
+
rendering_output_dir = temporary_folder_info['rendering_output_folder']
|
|
350
|
+
|
|
351
|
+
os.makedirs(rendering_output_dir,exist_ok=True)
|
|
352
|
+
|
|
353
|
+
detected_frame_files = visualize_detector_output.visualize_detector_output(
|
|
354
|
+
detector_output_path=options.output_json_file,
|
|
355
|
+
out_dir=rendering_output_dir,
|
|
356
|
+
images_dir=frame_output_folder,
|
|
357
|
+
confidence_threshold=options.rendering_confidence_threshold)
|
|
358
|
+
|
|
359
|
+
# Combine into a video
|
|
360
|
+
if options.frame_sample is None:
|
|
361
|
+
rendering_fs = Fs
|
|
362
|
+
else:
|
|
363
|
+
rendering_fs = Fs / options.frame_sample
|
|
364
|
+
|
|
365
|
+
print('Rendering {} frames to {} at {} fps (original video {} fps)'.format(
|
|
366
|
+
len(detected_frame_files), options.output_video_file,rendering_fs,Fs))
|
|
367
|
+
frames_to_video(detected_frame_files, rendering_fs, options.output_video_file,
|
|
368
|
+
codec_spec=options.fourcc)
|
|
369
|
+
|
|
370
|
+
# Possibly clean up rendered frames
|
|
371
|
+
_clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
|
|
372
|
+
|
|
373
|
+
# ...if we're rendering video
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
## (Optionally) delete the extracted frames
|
|
377
|
+
_clean_up_extracted_frames(options, frame_output_folder, frame_filenames)
|
|
378
|
+
|
|
379
|
+
# ...process_video()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def process_video_folder(options):
|
|
383
|
+
"""
|
|
384
|
+
Process a folder of videos through MD
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
options (ProcessVideoOptions): all the parameters used to control this process,
|
|
388
|
+
including filenames; see ProcessVideoOptions for details
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
## Validate options
|
|
392
|
+
|
|
393
|
+
assert os.path.isdir(options.input_video_file), \
|
|
394
|
+
'{} is not a folder'.format(options.input_video_file)
|
|
395
|
+
|
|
396
|
+
assert options.output_json_file is not None, \
|
|
397
|
+
'When processing a folder, you must specify an output .json file'
|
|
398
|
+
|
|
399
|
+
assert options.output_json_file.endswith('.json')
|
|
400
|
+
video_json = options.output_json_file
|
|
401
|
+
frames_json = options.output_json_file.replace('.json','.frames.json')
|
|
402
|
+
os.makedirs(os.path.dirname(video_json),exist_ok=True)
|
|
403
|
+
|
|
404
|
+
# Track whether frame and rendering folders were created by this script
|
|
405
|
+
caller_provided_frame_output_folder = (options.frame_folder is not None)
|
|
406
|
+
caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
|
|
407
|
+
|
|
408
|
+
# This does not create any folders, just defines temporary folder names in
|
|
409
|
+
# case we need them.
|
|
410
|
+
temporary_folder_info = _select_temporary_output_folders(options)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
## Split every video into frames
|
|
414
|
+
|
|
415
|
+
if caller_provided_frame_output_folder:
|
|
416
|
+
frame_output_folder = options.frame_folder
|
|
417
|
+
else:
|
|
418
|
+
frame_output_folder = temporary_folder_info['frame_output_folder']
|
|
419
|
+
|
|
420
|
+
os.makedirs(frame_output_folder, exist_ok=True)
|
|
421
|
+
|
|
422
|
+
print('Extracting frames')
|
|
423
|
+
frame_filenames, Fs, video_filenames = \
|
|
424
|
+
video_folder_to_frames(input_folder=options.input_video_file,
|
|
425
|
+
output_folder_base=frame_output_folder,
|
|
426
|
+
recursive=options.recursive,
|
|
427
|
+
overwrite=(not options.reuse_frames_if_available),
|
|
428
|
+
n_threads=options.n_cores,
|
|
429
|
+
every_n_frames=options.frame_sample,
|
|
430
|
+
verbose=options.verbose,
|
|
431
|
+
quality=options.quality,
|
|
432
|
+
max_width=options.max_width)
|
|
433
|
+
|
|
434
|
+
image_file_names = list(itertools.chain.from_iterable(frame_filenames))
|
|
435
|
+
|
|
436
|
+
if len(image_file_names) == 0:
|
|
437
|
+
if len(video_filenames) == 0:
|
|
438
|
+
print('No videos found in folder {}'.format(options.input_video_file))
|
|
439
|
+
else:
|
|
440
|
+
print('No frames extracted from folder {}, this may be due to an '\
|
|
441
|
+
'unsupported video codec'.format(options.input_video_file))
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
if options.debug_max_frames is not None and options.debug_max_frames > 0:
|
|
445
|
+
image_file_names = image_file_names[0:options.debug_max_frames]
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
## Run MegaDetector on the extracted frames
|
|
449
|
+
|
|
450
|
+
if options.reuse_results_if_available and \
|
|
451
|
+
os.path.isfile(frames_json):
|
|
452
|
+
print('Bypassing inference, loading results from {}'.format(frames_json))
|
|
453
|
+
results = None
|
|
454
|
+
else:
|
|
455
|
+
print('Running MegaDetector')
|
|
456
|
+
results = run_detector_batch.load_and_run_detector_batch(
|
|
457
|
+
options.model_file, image_file_names,
|
|
458
|
+
confidence_threshold=options.json_confidence_threshold,
|
|
459
|
+
n_cores=options.n_cores,
|
|
460
|
+
class_mapping_filename=options.class_mapping_filename,
|
|
461
|
+
quiet=True)
|
|
462
|
+
|
|
463
|
+
run_detector_batch.write_results_to_file(
|
|
464
|
+
results, frames_json,
|
|
465
|
+
relative_path_base=frame_output_folder,
|
|
466
|
+
detector_file=options.model_file,
|
|
467
|
+
custom_metadata={'video_frame_rate':Fs})
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
## Convert frame-level results to video-level results
|
|
471
|
+
|
|
472
|
+
print('Converting frame-level results to video-level results')
|
|
473
|
+
frame_results_to_video_results(frames_json,video_json)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
## (Optionally) render output videos
|
|
477
|
+
|
|
478
|
+
if options.render_output_video:
|
|
479
|
+
|
|
480
|
+
# Render detections to images
|
|
481
|
+
if (caller_provided_rendering_output_folder):
|
|
482
|
+
rendering_output_dir = options.frame_rendering_folder
|
|
483
|
+
else:
|
|
484
|
+
rendering_output_dir = temporary_folder_info['rendering_output_folder']
|
|
485
|
+
|
|
486
|
+
os.makedirs(rendering_output_dir,exist_ok=True)
|
|
487
|
+
|
|
488
|
+
detected_frame_files = visualize_detector_output.visualize_detector_output(
|
|
489
|
+
detector_output_path=frames_json,
|
|
490
|
+
out_dir=rendering_output_dir,
|
|
491
|
+
images_dir=frame_output_folder,
|
|
492
|
+
confidence_threshold=options.rendering_confidence_threshold,
|
|
493
|
+
preserve_path_structure=True,
|
|
494
|
+
output_image_width=-1)
|
|
495
|
+
detected_frame_files = [s.replace('\\','/') for s in detected_frame_files]
|
|
496
|
+
|
|
497
|
+
# Choose an output folder
|
|
498
|
+
output_folder_is_input_folder = False
|
|
499
|
+
if options.output_video_file is not None:
|
|
500
|
+
if os.path.isfile(options.output_video_file):
|
|
501
|
+
raise ValueError('Rendering videos for a folder, but an existing file was specified as output')
|
|
502
|
+
elif options.output_video_file == options.input_video_file:
|
|
503
|
+
output_folder_is_input_folder = True
|
|
504
|
+
output_video_folder = options.input_video_file
|
|
505
|
+
else:
|
|
506
|
+
os.makedirs(options.output_video_file,exist_ok=True)
|
|
507
|
+
output_video_folder = options.output_video_file
|
|
508
|
+
else:
|
|
509
|
+
output_folder_is_input_folder = True
|
|
510
|
+
output_video_folder = options.input_video_file
|
|
511
|
+
|
|
512
|
+
# For each video
|
|
513
|
+
#
|
|
514
|
+
# TODO: parallelize this loop
|
|
515
|
+
#
|
|
516
|
+
# i_video=0; input_video_file_abs = video_filenames[i_video]
|
|
517
|
+
for i_video,input_video_file_abs in enumerate(video_filenames):
|
|
518
|
+
|
|
519
|
+
video_fs = Fs[i_video]
|
|
520
|
+
|
|
521
|
+
if options.frame_sample is None:
|
|
522
|
+
rendering_fs = video_fs
|
|
523
|
+
else:
|
|
524
|
+
rendering_fs = video_fs / options.frame_sample
|
|
525
|
+
|
|
526
|
+
input_video_file_relative = os.path.relpath(input_video_file_abs,options.input_video_file)
|
|
527
|
+
video_frame_output_folder = os.path.join(rendering_output_dir,input_video_file_relative)
|
|
528
|
+
|
|
529
|
+
video_frame_output_folder = video_frame_output_folder.replace('\\','/')
|
|
530
|
+
assert os.path.isdir(video_frame_output_folder), \
|
|
531
|
+
'Could not find frame folder for video {}'.format(input_video_file_relative)
|
|
532
|
+
|
|
533
|
+
# Find the corresponding rendered frame folder
|
|
534
|
+
video_frame_files = [fn for fn in detected_frame_files if \
|
|
535
|
+
fn.startswith(video_frame_output_folder)]
|
|
536
|
+
assert len(video_frame_files) > 0, 'Could not find rendered frames for video {}'.format(
|
|
537
|
+
input_video_file_relative)
|
|
538
|
+
|
|
539
|
+
# Select the output filename for the rendered video
|
|
540
|
+
if output_folder_is_input_folder:
|
|
541
|
+
video_output_file = insert_before_extension(input_video_file_abs,'annotated','_')
|
|
542
|
+
else:
|
|
543
|
+
video_output_file = os.path.join(output_video_folder,input_video_file_relative)
|
|
544
|
+
|
|
545
|
+
os.makedirs(os.path.dirname(video_output_file),exist_ok=True)
|
|
546
|
+
|
|
547
|
+
# Create the output video
|
|
548
|
+
print('Rendering detections for video {} to {} at {} fps (original video {} fps)'.format(
|
|
549
|
+
input_video_file_relative,video_output_file,rendering_fs,video_fs))
|
|
550
|
+
frames_to_video(video_frame_files, rendering_fs, video_output_file, codec_spec=options.fourcc)
|
|
551
|
+
|
|
552
|
+
# ...for each video
|
|
553
|
+
|
|
554
|
+
# Possibly clean up rendered frames
|
|
555
|
+
_clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
|
|
556
|
+
|
|
557
|
+
# ...if we're rendering video
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
## (Optionally) delete the extracted frames
|
|
561
|
+
_clean_up_extracted_frames(options, frame_output_folder, image_file_names)
|
|
562
|
+
|
|
563
|
+
# ...process_video_folder()
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def options_to_command(options):
|
|
567
|
+
"""
|
|
568
|
+
Convert a ProcessVideoOptions obejct to a corresponding command line.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
options (ProcessVideoOptions): the options set to render as a command line
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
str: the command line coresponding to [options]
|
|
575
|
+
|
|
576
|
+
:meta private:
|
|
577
|
+
"""
|
|
578
|
+
cmd = 'python process_video.py'
|
|
579
|
+
cmd += ' "' + options.model_file + '"'
|
|
580
|
+
cmd += ' "' + options.input_video_file + '"'
|
|
581
|
+
|
|
582
|
+
if options.recursive:
|
|
583
|
+
cmd += ' --recursive'
|
|
584
|
+
if options.frame_folder is not None:
|
|
585
|
+
cmd += ' --frame_folder' + ' "' + options.frame_folder + '"'
|
|
586
|
+
if options.frame_rendering_folder is not None:
|
|
587
|
+
cmd += ' --frame_rendering_folder' + ' "' + options.frame_rendering_folder + '"'
|
|
588
|
+
if options.output_json_file is not None:
|
|
589
|
+
cmd += ' --output_json_file' + ' "' + options.output_json_file + '"'
|
|
590
|
+
if options.output_video_file is not None:
|
|
591
|
+
cmd += ' --output_video_file' + ' "' + options.output_video_file + '"'
|
|
592
|
+
if options.keep_extracted_frames:
|
|
593
|
+
cmd += ' --keep_extracted_frames'
|
|
594
|
+
if options.reuse_results_if_available:
|
|
595
|
+
cmd += ' --reuse_results_if_available'
|
|
596
|
+
if options.reuse_frames_if_available:
|
|
597
|
+
cmd += ' --reuse_frames_if_available'
|
|
598
|
+
if options.render_output_video:
|
|
599
|
+
cmd += ' --render_output_video'
|
|
600
|
+
if options.keep_rendered_frames:
|
|
601
|
+
cmd += ' --keep_rendered_frames'
|
|
602
|
+
if options.rendering_confidence_threshold is not None:
|
|
603
|
+
cmd += ' --rendering_confidence_threshold ' + str(options.rendering_confidence_threshold)
|
|
604
|
+
if options.json_confidence_threshold is not None:
|
|
605
|
+
cmd += ' --json_confidence_threshold ' + str(options.json_confidence_threshold)
|
|
606
|
+
if options.n_cores is not None:
|
|
607
|
+
cmd += ' --n_cores ' + str(options.n_cores)
|
|
608
|
+
if options.frame_sample is not None:
|
|
609
|
+
cmd += ' --frame_sample ' + str(options.frame_sample)
|
|
610
|
+
if options.debug_max_frames is not None:
|
|
611
|
+
cmd += ' --debug_max_frames ' + str(options.debug_max_frames)
|
|
612
|
+
if options.class_mapping_filename is not None:
|
|
613
|
+
cmd += ' --class_mapping_filename ' + str(options.class_mapping_filename)
|
|
614
|
+
if options.fourcc is not None:
|
|
615
|
+
cmd += ' --fourcc ' + options.fourcc
|
|
616
|
+
if options.quality is not None:
|
|
617
|
+
cmd += ' --quality ' + str(options.quality)
|
|
618
|
+
if options.max_width is not None:
|
|
619
|
+
cmd += ' --max_width ' + str(options.max_width)
|
|
620
|
+
if options.verbose:
|
|
621
|
+
cmd += ' --verbose'
|
|
622
|
+
if options.force_extracted_frame_folder_deletion:
|
|
623
|
+
cmd += ' --force_extracted_frame_folder_deletion'
|
|
624
|
+
if options.force_rendered_frame_folder_deletion:
|
|
625
|
+
cmd += ' --force_rendered_frame_folder_deletion'
|
|
626
|
+
|
|
627
|
+
return cmd
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
#%% Interactive driver
|
|
631
|
+
|
|
632
|
+
if False:
|
|
633
|
+
|
|
634
|
+
#%% Process a folder of videos
|
|
635
|
+
|
|
636
|
+
model_file = 'MDV5A'
|
|
637
|
+
input_dir = r'g:\temp\test-videos'
|
|
638
|
+
output_base = r'g:\temp\video_test'
|
|
639
|
+
frame_folder = os.path.join(output_base,'frames')
|
|
640
|
+
rendering_folder = os.path.join(output_base,'rendered-frames')
|
|
641
|
+
output_json_file = os.path.join(output_base,'video-test.json')
|
|
642
|
+
output_video_folder = os.path.join(output_base,'output_videos')
|
|
643
|
+
|
|
644
|
+
print('Processing folder {}'.format(input_dir))
|
|
645
|
+
|
|
646
|
+
options = ProcessVideoOptions()
|
|
647
|
+
options.model_file = model_file
|
|
648
|
+
options.input_video_file = input_dir
|
|
649
|
+
options.output_video_file = output_video_folder
|
|
650
|
+
options.output_json_file = output_json_file
|
|
651
|
+
options.recursive = True
|
|
652
|
+
options.reuse_frames_if_available = False
|
|
653
|
+
options.reuse_results_if_available = False
|
|
654
|
+
options.quality = 90
|
|
655
|
+
options.frame_sample = 10
|
|
656
|
+
options.max_width = 1280
|
|
657
|
+
options.n_cores = 5
|
|
658
|
+
options.verbose = True
|
|
659
|
+
options.render_output_video = True
|
|
660
|
+
|
|
661
|
+
options.frame_folder = None # frame_folder
|
|
662
|
+
options.frame_rendering_folder = None # rendering_folder
|
|
663
|
+
|
|
664
|
+
options.keep_extracted_frames = False
|
|
665
|
+
options.keep_rendered_frames = False
|
|
666
|
+
options.force_extracted_frame_folder_deletion = True
|
|
667
|
+
options.force_rendered_frame_folder_deletion = True
|
|
668
|
+
|
|
669
|
+
# options.confidence_threshold = 0.15
|
|
670
|
+
options.fourcc = 'mp4v'
|
|
671
|
+
|
|
672
|
+
cmd = options_to_command(options); print(cmd)
|
|
673
|
+
|
|
674
|
+
import clipboard; clipboard.copy(cmd)
|
|
675
|
+
|
|
676
|
+
if False:
|
|
677
|
+
process_video_folder(options)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
#%% Process a single video
|
|
681
|
+
|
|
682
|
+
fn = r'g:\temp\test-videos\person_and_dog\DSCF0056.AVI'
|
|
683
|
+
model_file = 'MDV5A'
|
|
684
|
+
input_video_file = fn
|
|
685
|
+
|
|
686
|
+
output_base = r'g:\temp\video_test'
|
|
687
|
+
frame_folder = os.path.join(output_base,'frames')
|
|
688
|
+
rendering_folder = os.path.join(output_base,'rendered-frames')
|
|
689
|
+
output_json_file = os.path.join(output_base,'video-test.json')
|
|
690
|
+
output_video_file = os.path.join(output_base,'output_videos.mp4')
|
|
691
|
+
|
|
692
|
+
options = ProcessVideoOptions()
|
|
693
|
+
options.model_file = model_file
|
|
694
|
+
options.input_video_file = input_video_file
|
|
695
|
+
options.render_output_video = True
|
|
696
|
+
options.output_video_file = output_video_file
|
|
697
|
+
|
|
698
|
+
options.verbose = True
|
|
699
|
+
|
|
700
|
+
options.quality = 75
|
|
701
|
+
options.frame_sample = None # 10
|
|
702
|
+
options.max_width = 600
|
|
703
|
+
|
|
704
|
+
options.frame_folder = None # frame_folder
|
|
705
|
+
options.frame_rendering_folder = None # rendering_folder
|
|
706
|
+
|
|
707
|
+
options.keep_extracted_frames = False
|
|
708
|
+
options.keep_rendered_frames = False
|
|
709
|
+
options.force_extracted_frame_folder_deletion = True
|
|
710
|
+
options.force_rendered_frame_folder_deletion = True
|
|
711
|
+
|
|
712
|
+
# options.confidence_threshold = 0.15
|
|
713
|
+
options.fourcc = 'mp4v'
|
|
714
|
+
|
|
715
|
+
cmd = options_to_command(options); print(cmd)
|
|
716
|
+
|
|
717
|
+
import clipboard; clipboard.copy(cmd)
|
|
718
|
+
|
|
719
|
+
if False:
|
|
720
|
+
process_video(options)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
#%% Command-line driver
|
|
724
|
+
|
|
725
|
+
def main():
|
|
726
|
+
|
|
727
|
+
default_options = ProcessVideoOptions()
|
|
728
|
+
|
|
729
|
+
parser = argparse.ArgumentParser(description=(
|
|
730
|
+
'Run MegaDetector on each frame (or every Nth frame) in a video (or folder of videos), optionally '\
|
|
731
|
+
'producing a new video with detections annotated'))
|
|
732
|
+
|
|
733
|
+
parser.add_argument('model_file', type=str,
|
|
734
|
+
help='MegaDetector model file (.pt or .pb) or model name (e.g. "MDV5A")')
|
|
735
|
+
|
|
736
|
+
parser.add_argument('input_video_file', type=str,
|
|
737
|
+
help='video file (or folder) to process')
|
|
738
|
+
|
|
739
|
+
parser.add_argument('--recursive', action='store_true',
|
|
740
|
+
help='recurse into [input_video_file]; only meaningful if a folder '\
|
|
741
|
+
'is specified as input')
|
|
742
|
+
|
|
743
|
+
parser.add_argument('--frame_folder', type=str, default=None,
|
|
744
|
+
help='folder to use for intermediate frame storage, defaults to a folder '\
|
|
745
|
+
'in the system temporary folder')
|
|
746
|
+
|
|
747
|
+
parser.add_argument('--frame_rendering_folder', type=str, default=None,
|
|
748
|
+
help='folder to use for rendered frame storage, defaults to a folder in '\
|
|
749
|
+
'the system temporary folder')
|
|
750
|
+
|
|
751
|
+
parser.add_argument('--output_json_file', type=str,
|
|
752
|
+
default=None, help='.json output file, defaults to [video file].json')
|
|
753
|
+
|
|
754
|
+
parser.add_argument('--output_video_file', type=str,
|
|
755
|
+
default=None, help='video output file (or folder), defaults to '\
|
|
756
|
+
'[video file].mp4 for files, or [video file]_annotated for folders')
|
|
757
|
+
|
|
758
|
+
parser.add_argument('--keep_extracted_frames',
|
|
759
|
+
action='store_true', help='Disable the deletion of extracted frames')
|
|
760
|
+
|
|
761
|
+
parser.add_argument('--reuse_frames_if_available',
|
|
762
|
+
action='store_true', help="Don't extract frames that are already available in the frame extraction folder")
|
|
763
|
+
|
|
764
|
+
parser.add_argument('--reuse_results_if_available',
|
|
765
|
+
action='store_true', help='If the output .json files exists, and this flag is set,'\
|
|
766
|
+
'we\'ll skip running MegaDetector')
|
|
767
|
+
|
|
768
|
+
parser.add_argument('--render_output_video', action='store_true',
|
|
769
|
+
help='enable video output rendering (not rendered by default)')
|
|
770
|
+
|
|
771
|
+
parser.add_argument('--fourcc', default=default_fourcc,
|
|
772
|
+
help='fourcc code to use for video encoding (default {}), only used if render_output_video is True'.format(
|
|
773
|
+
default_fourcc))
|
|
774
|
+
|
|
775
|
+
parser.add_argument('--keep_rendered_frames',
|
|
776
|
+
action='store_true', help='Disable the deletion of rendered (w/boxes) frames')
|
|
777
|
+
|
|
778
|
+
parser.add_argument('--force_extracted_frame_folder_deletion',
|
|
779
|
+
action='store_true', help='By default, when keep_extracted_frames is False, we '\
|
|
780
|
+
'delete the frames, but leave the (probably-empty) folder in place. This option '\
|
|
781
|
+
'forces deletion of the folder as well. Use at your own risk; does not check '\
|
|
782
|
+
'whether other files were present in the folder.')
|
|
783
|
+
|
|
784
|
+
parser.add_argument('--force_rendered_frame_folder_deletion',
|
|
785
|
+
action='store_true', help='By default, when keep_rendered_frames is False, we '\
|
|
786
|
+
'delete the frames, but leave the (probably-empty) folder in place. This option '\
|
|
787
|
+
'forces deletion of the folder as well. Use at your own risk; does not check '\
|
|
788
|
+
'whether other files were present in the folder.')
|
|
789
|
+
|
|
790
|
+
parser.add_argument('--rendering_confidence_threshold', type=float,
|
|
791
|
+
default=None, help="don't render boxes with confidence below this threshold (defaults to choosing based on the MD version)")
|
|
792
|
+
|
|
793
|
+
parser.add_argument('--json_confidence_threshold', type=float,
|
|
794
|
+
default=0.0, help="don't include boxes in the .json file with confidence "\
|
|
795
|
+
'below this threshold (default {})'.format(
|
|
796
|
+
default_options.json_confidence_threshold))
|
|
797
|
+
|
|
798
|
+
parser.add_argument('--n_cores', type=int,
|
|
799
|
+
default=1, help='Number of cores to use for frame separation and detection. '\
|
|
800
|
+
'If using a GPU, this option will be respected for frame separation but '\
|
|
801
|
+
'ignored for detection. Only relevant to frame separation when processing '\
|
|
802
|
+
'a folder.')
|
|
803
|
+
|
|
804
|
+
parser.add_argument('--frame_sample', type=int,
|
|
805
|
+
default=None, help='process every Nth frame (defaults to every frame)')
|
|
806
|
+
|
|
807
|
+
parser.add_argument('--quality', type=int,
|
|
808
|
+
default=default_options.quality,
|
|
809
|
+
help='JPEG quality for extracted frames (defaults to {})'.format(
|
|
810
|
+
default_options.quality))
|
|
811
|
+
|
|
812
|
+
parser.add_argument('--max_width', type=int,
|
|
813
|
+
default=default_options.max_width,
|
|
814
|
+
help='Resize frames larger than this before writing (defaults to {})'.format(
|
|
815
|
+
default_options.max_width))
|
|
816
|
+
|
|
817
|
+
parser.add_argument('--debug_max_frames', type=int,
|
|
818
|
+
default=-1, help='Trim to N frames for debugging (impacts model execution, '\
|
|
819
|
+
'not frame rendering)')
|
|
820
|
+
|
|
821
|
+
parser.add_argument('--class_mapping_filename',
|
|
822
|
+
type=str,
|
|
823
|
+
default=None, help='Use a non-default class mapping, supplied in a .json file '\
|
|
824
|
+
'with a dictionary mapping int-strings to strings. This will also disable '\
|
|
825
|
+
'the addition of "1" to all category IDs, so your class mapping should start '\
|
|
826
|
+
'at zero.')
|
|
827
|
+
|
|
828
|
+
parser.add_argument('--verbose', action='store_true',
|
|
829
|
+
help='Enable additional debug output')
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
if len(sys.argv[1:]) == 0:
|
|
833
|
+
parser.print_help()
|
|
834
|
+
parser.exit()
|
|
835
|
+
|
|
836
|
+
args = parser.parse_args()
|
|
837
|
+
options = ProcessVideoOptions()
|
|
838
|
+
args_to_object(args,options)
|
|
839
|
+
|
|
840
|
+
if os.path.isdir(options.input_video_file):
|
|
841
|
+
process_video_folder(options)
|
|
842
|
+
else:
|
|
843
|
+
process_video(options)
|
|
844
|
+
|
|
845
|
+
if __name__ == '__main__':
|
|
846
|
+
main()
|