diff --git a/TODO.md b/TODO.md index b5159d45..e9958907 100644 --- a/TODO.md +++ b/TODO.md @@ -54,17 +54,6 @@ E1 !maintenance -## Add --force_model_download to run_md_and_speciesnet - -run_detector_batch supports a --force_model_download argument to handle the case where model weights were partially downloaded; add a corresponding option to run_md_and_speciesnet. It should apply to both the MD weights and the SpeciesNet weights. - -P0 - -E0 - -!feature - - ## Graceful handling of small images during tiling When running tiled inference, if either dimension of an image is smaller than the tiling size, that image fails. This is OK, it's correctly recorded as an inference failure, but in most cases I would rather it fall back to a smaller tile size in that case. @@ -443,21 +432,6 @@ E3 !feature -## Reference result updates - -* Reduce complexity of reference results: MD's test harness relies on .json files with pre-generated results for MDv5a and MDv5b, for a reference set of images and videos. Because output varies slightly between PyTorch versions and between hardware environments, I have a number of results files. This has gotten too complicated; remove most of the results files and increase the allowed tolerance during testing. -* Add test results for MD1000 models: MD's test harness only has results for MDv5, so it tests the not-crashing-ness of the other models, but it does not test correctness. Add test results for other MD1000 models. -* Vehicle images: none of the test images include vehicles; add vehicle images to testing, including human/vehicle and animal/vehicle images -* Images with lat/lon information in EXIF metadata; make sure EXIF extraction (especially GPS location) is working correctly. -* The "magic zebra image" that causes problems on M1 Pro machines; this should be called out as a dedicated single-command test case - -P0 - -E1 - -!testing - - ## Test coverage improvements This is a placeholder for generally evaluating md_tests and the pytest harness, and deciding which scripts need additional testing. Effort is highly variable; for example, adding tests for run_md_and_speciesnet is important and very easy. Adding tests for postprocess_batch_results that actually verify correctness is a pain. This work item almost certainly starts with asking AI what modules are not covered (or poorly covered) by tests. @@ -678,17 +652,6 @@ E1 !feature -## Update Colab - -Nothing is "wrong" with the [MegaDetector Colab](https://github.com/agentmorris/MegaDetector/blob/main/notebooks/megadetector_colab.ipynb), but it hasn't been updated in a while. It doesn't mention MDv1000 or SpeciesNet; it would be helpful to just give the Colab a once-over, make sure it's still in good shape, and add optional cells that demonstrate MDv1000 use and SpeciesNet inference (via run_md_and_speciesnet). - -P0 - -E0 - -!feature - - ## Explore compiled PyTorch [torch.compile](https://pytorch.org/tutorials/intermediate/torch_compile_tutorial.html) was introduced in 2023, but I haven't evaluated it for MegaDetector (or SpeciesNet). Evaluate it for both MegaDetector and SpeciesNet. diff --git a/megadetector/data_management/lila/get_lila_annotation_counts.py b/megadetector/data_management/lila/get_lila_annotation_counts.py index 672835ef..dc17154a 100644 --- a/megadetector/data_management/lila/get_lila_annotation_counts.py +++ b/megadetector/data_management/lila/get_lila_annotation_counts.py @@ -95,7 +95,7 @@ dataset_to_categories = {} -# ds_name = 'NACTI' +# ds_name = 'SWG Camera Traps' for ds_name in metadata_table.keys(): taxonomy_mapping_available = (ds_name in datasets_with_taxonomy_mapping) @@ -116,6 +116,12 @@ # Collect list of categories and mappings to category name categories = data['categories'] + # In some cases, a "count" field is in the actual .json file, remove this + # so we can re-count + for c in categories: + if 'count' in c: + del c['count'] + category_id_to_count = defaultdict(int) annotations = data['annotations'] @@ -125,9 +131,11 @@ # c = categories[0] for c in categories: + count = category_id_to_count[c['id']] if 'count' in c: - assert 'bbox' in ds_name or c['count'] == count + assert ('bbox' in ds_name) or (c['count'] == count) + c['count'] = count # Don't do taxonomy mapping for bbox data sets, which are sometimes just binary and are @@ -148,6 +156,8 @@ assert sn is not None and len(sn) > 0 c['scientific_name_from_taxonomy_mapping'] = sn + # ...for each category + dataset_to_categories[ds_name] = categories # ...for each dataset diff --git a/megadetector/detection/process_video.py b/megadetector/detection/process_video.py index 42dde962..8f2107ad 100644 --- a/megadetector/detection/process_video.py +++ b/megadetector/detection/process_video.py @@ -87,6 +87,11 @@ def __init__(self): #: Detector-specific options self.detector_options = None + #: Force downloading the model file if a named model (e.g. "MDV5A") is supplied, + #: even if the local file already exists (typically to overwrite a corrupted + #: model file) + self.force_model_download = False + #: Write a checkpoint file (to resume processing later) every N videos; #: set to -1 (default) to disable checkpointing self.checkpoint_frequency = -1 @@ -153,7 +158,9 @@ def process_videos(options): if options.verbose: print('Processing videos from input source {}'.format(options.input_video_file)) - detector = load_detector(options.model_file,detector_options=options.detector_options) + detector = load_detector(options.model_file, + force_model_download=options.force_model_download, + detector_options=options.detector_options) def frame_callback(image_np,image_id): return detector.generate_detections_one_image(image_np, @@ -434,6 +441,12 @@ def main(): # noqa default='', help='Detector-specific options, as a space-separated list of key-value pairs') + parser.add_argument( + '--force_model_download', + action='store_true', + help=('If a named model (e.g. "MDV5A") is supplied, force a download of that model even if the ' +\ + 'local file already exists (typically to overwrite a corrupted model file).')) + parser.add_argument( '--checkpoint_frequency', type=int, diff --git a/megadetector/detection/run_detector_batch.py b/megadetector/detection/run_detector_batch.py index df35ff4d..83e764f5 100644 --- a/megadetector/detection/run_detector_batch.py +++ b/megadetector/detection/run_detector_batch.py @@ -1110,7 +1110,7 @@ def load_and_run_detector_batch(model_file, augment (bool, optional): enable image augmentation force_model_download (bool, optional): force downloading the model file if a named model (e.g. "MDV5A") is supplied, even if the local file already - exists + exists (typically to handle the case where the model file is corrupted). detector_options (dict, optional): key/value pairs that are interpreted differently by different detectors. Can also be a list of k=v pairs, or a comma-delimited string containing a list of k=v pairs. @@ -1889,7 +1889,7 @@ def main(): # noqa '--force_model_download', action='store_true', help=('If a named model (e.g. "MDV5A") is supplied, force a download of that model even if the ' +\ - 'local file already exists.')) + 'local file already exists (typically to overwrite a corrupted model model).')) parser.add_argument( '--previous_results_file', type=str, diff --git a/megadetector/detection/run_md_and_speciesnet.py b/megadetector/detection/run_md_and_speciesnet.py index bccb0601..9cc5d9ab 100644 --- a/megadetector/detection/run_md_and_speciesnet.py +++ b/megadetector/detection/run_md_and_speciesnet.py @@ -188,6 +188,13 @@ def __init__(self): #: Include raw (pre-rollup/geofence) classification scores in output self.include_raw_classifications = False + #: Force downloading the detector and classifier model files, even if the + #: local files already exist (typically to overwrite corrupted model files). + #: + #: Only relevant for named/remote models, i.e. this has no effect for models + #: that are specified as local files or folders. + self.force_model_download = False + if self.time_sample is None and self.frame_sample is None: self.time_sample = DEFAULT_SECONDS_PER_VIDEO_FRAME @@ -939,7 +946,8 @@ def _run_detection_step(source_folder: str, skip_images: bool = False, skip_video: bool = False, frame_sample: int = None, - time_sample: float = None) -> str: + time_sample: float = None, + force_model_download: bool = False) -> str: """ Run MegaDetector on all images/videos in [source_folder]. @@ -956,6 +964,9 @@ def _run_detection_step(source_folder: str, skip_video (bool, optional): ignore videos, only process images frame_sample (int, optional): sample every Nth frame from videos time_sample (float, optional): sample frames every N seconds from videos + force_model_download (bool, optional): force downloading the detector model file + if a named model (e.g. "MDV5A") is supplied, even if the local file already + exists (typically to overwrite a corrupted model file) """ print('Starting detection step...') @@ -1009,7 +1020,8 @@ def _run_detection_step(source_folder: str, include_exif_tags=None, loader_workers=detector_worker_threads, preprocess_on_image_queue=True, - use_threads_for_queue=use_threads_for_queue + use_threads_for_queue=use_threads_for_queue, + force_model_download=force_model_download ) # Write image results to temporary file @@ -1029,9 +1041,14 @@ def _run_detection_step(source_folder: str, print('Running MegaDetector on {} videos...'.format(len(video_files))) + # If we also had images to process, we already forced a download of the + # detector model above, so there's no need to download it again here. + force_model_download_for_videos = force_model_download and (len(image_files) == 0) + # Set up video processing options video_options = ProcessVideoOptions() video_options.model_file = detector_model + video_options.force_model_download = force_model_download_for_videos video_options.input_video_file = source_folder video_options.output_json_file = detector_output_file.replace('.json', '_videos.json') video_options.json_confidence_threshold = detection_confidence_threshold @@ -1078,7 +1095,8 @@ def _run_classification_step(detector_results_file: str, top_n_scores: int = DEFAULT_TOP_N_SCORES, worker_type: str = DEFAULT_WORKER_TYPE, include_raw_classifications: bool = False, - rollup_target_confidence: float = DEFAULT_ROLLUP_TARGET_CONFIDENCE): + rollup_target_confidence: float = DEFAULT_ROLLUP_TARGET_CONFIDENCE, + force_model_download: bool = False): """ Run SpeciesNet classification on detections from MegaDetector results. @@ -1099,6 +1117,9 @@ def _run_classification_step(detector_results_file: str, classification scores in output rollup_target_confidence (float, optional): target confidence threshold for taxonomic rollup. Ignored if enable_rollup is False. + force_model_download (bool, optional): force downloading the classifier model files + if a remote model (e.g. a "kaggle:" or "hf:" identifier) is supplied, even if + the local files already exist (typically to overwrite corrupted model files) """ print('Starting classification step...') @@ -1118,6 +1139,22 @@ def _run_classification_step(detector_results_file: str, print('Using SpeciesNet classifier: {}'.format(classifier_model)) + # The classifier gets loaded in several places below (in the main thread and/or in + # worker threads/processes, depending on [worker_type]), and it doesn't make sense + # to force a download in all of those places. Instead, if we've been asked to force + # a download, we load a throwaway instance here, on the CPU, just to make sure the + # model files are freshly downloaded (and loadable) before we do anything else. + if force_model_download: + + print('Forcing a download of classifier model {}'.format(classifier_model)) + throwaway_classifier = SpeciesNetClassifier(classifier_model, + device='cpu', + force_model_download=True) + del throwaway_classifier + print('Finished forced download of classifier model {}'.format(classifier_model)) + + # ...if we need to force a model download + # Set multiprocessing start method to 'spawn' for CUDA compatibility if worker_type == 'process': original_start_method = multiprocessing.get_start_method() @@ -1450,7 +1487,8 @@ def run_md_and_speciesnet(options): skip_video=options.skip_video, frame_sample=options.frame_sample, time_sample=options.time_sample, - worker_type=options.worker_type + worker_type=options.worker_type, + force_model_download=options.force_model_download ) # Run SpeciesNet @@ -1467,7 +1505,8 @@ def run_md_and_speciesnet(options): admin1_region=options.admin1_region, worker_type=options.worker_type, include_raw_classifications=options.include_raw_classifications, - rollup_target_confidence=options.rollup_target_confidence + rollup_target_confidence=options.rollup_target_confidence, + force_model_download=options.force_model_download ) elapsed_time = time.time() - start_time @@ -1584,6 +1623,11 @@ def main(): parser.add_argument('--include_raw_classifications', action='store_true', help='Include raw (pre-rollup/geofence) classification scores in output') + parser.add_argument('--force_model_download', + action='store_true', + help='Force a download of both the detector and classifier models, even if ' + \ + 'the local model files already exist (typically to overwrite corrupted ' + \ + 'model files)') if len(sys.argv[1:]) == 0: parser.print_help() diff --git a/megadetector/taxonomy_mapping/map_new_lila_datasets.py b/megadetector/taxonomy_mapping/map_new_lila_datasets.py index f2f0a810..7a6519a9 100644 --- a/megadetector/taxonomy_mapping/map_new_lila_datasets.py +++ b/megadetector/taxonomy_mapping/map_new_lila_datasets.py @@ -15,10 +15,10 @@ # Created by get_lila_category_list.py input_lila_category_list_file = os.path.expanduser('~/lila/lila_categories_list/lila_dataset_to_categories.json') -output_file = os.path.expanduser('~/lila/lila_additions_2026.06.17.csv') +output_file = os.path.expanduser('~/lila/lila_additions_2026.09.01.csv') datasets_to_map = [ - 'AMMonitor Camera Traps' + 'Duck Pictures in Wetlands' ] @@ -192,7 +192,7 @@ # Use this when an iNat match includes an empty subgenus with the same name as the genus n_levels_to_pop = 0 - q = 'animalia' + q = 'cygnus' taxonomy_preference = 'inat' m = get_preferred_taxonomic_match(q,taxonomy_preference) diff --git a/megadetector/taxonomy_mapping/preview_lila_taxonomy.py b/megadetector/taxonomy_mapping/preview_lila_taxonomy.py index 5e57125b..3bee2e44 100644 --- a/megadetector/taxonomy_mapping/preview_lila_taxonomy.py +++ b/megadetector/taxonomy_mapping/preview_lila_taxonomy.py @@ -16,7 +16,7 @@ import pandas as pd # lila_taxonomy_file = r"c:\git\agentmorrisprivate\lila-taxonomy\lila-taxonomy-mapping.csv" -lila_taxonomy_file = os.path.expanduser('~/lila/lila_additions_2026.06.17.csv') +lila_taxonomy_file = os.path.expanduser('~/lila/lila_additions_2026.09.01.csv') preview_base = os.path.expanduser('~/lila/lila_taxonomy_preview') os.makedirs(preview_base,exist_ok=True) diff --git a/megadetector/utils/md_tests.py b/megadetector/utils/md_tests.py index a66d7ecb..17533b82 100644 --- a/megadetector/utils/md_tests.py +++ b/megadetector/utils/md_tests.py @@ -152,21 +152,14 @@ def __init__(self): #%% Support functions -def get_expected_results_filename(gpu_is_available, - model_string='mdv5a', +def get_expected_results_filename(model_string='mdv5a', test_type='image', augment=False, options=None): """ - Expected results vary just a little across inference environments, particularly - between PT 1.x and 2.x, so when making sure things are working acceptably, we - compare to a reference file that matches the current environment. - - This function gets the correct filename to compare to current results, depending - on whether a GPU is available. + This function gets the correct filename to compare to current results. Args: - gpu_is_available (bool): whether a GPU is available model_string (str, optional): the model for which we're retrieving expected results test_type (str, optional): the test type we're running ("image" or "video") augment (bool, optional): whether we're running this test with image augmentation @@ -177,40 +170,17 @@ def get_expected_results_filename(gpu_is_available, data zipfile) """ - if gpu_is_available: - hw_string = 'gpu' - else: - hw_string = 'cpu' - import torch - torch_version = str(torch.__version__) - if torch_version.startswith('1'): - assert torch_version == '1.10.1', 'Only tested against PT 1.10.1 and PT 2.x' - pt_string = 'pt1.10.1' + if augment: + aug_string = 'augment' else: - assert torch_version.startswith('2'), 'Unknown torch version: {}'.format(torch_version) - pt_string = 'pt2.x' - - # A hack for now to account for the fact that even with acceleration enabled and PT2 - # installed, Apple silicon appears to provide the same results as CPU/PT1 inference - try: - import torch - m1_inference = torch.backends.mps.is_built and torch.backends.mps.is_available() - if m1_inference: - print('I appear to be running on M1/M2 hardware, using pt1/cpu as the reference results') - hw_string = 'cpu' - pt_string = 'pt1.10.1' - except Exception: - pass + aug_string = 'no_augment' - aug_string = '' - if augment: - aug_string = 'augment-' + assert test_type in ('image','video') - # We only have a single set of video results if test_type == 'image': - fn = '{}-{}{}-{}-{}.json'.format(model_string,aug_string,test_type,hw_string,pt_string) + fn = '{}-{}-{}.json'.format(model_string,test_type,aug_string) else: - fn = '{}-{}.json'.format(model_string,test_type) + fn = '{}-{}-{}.json'.format(model_string,test_type,aug_string) if options is not None and options.scratch_dir is not None: fn = os.path.join(options.scratch_dir,fn) @@ -822,6 +792,9 @@ def run_python_tests(options): ## Make sure our tests are doing what we think they're doing from megadetector.detection import pytorch_detector + + # We're not actually going to set a non-default compatibility mode, we're just + # going to make sure that a dummy string is correctly picked up. pytorch_detector.require_non_default_compatibility_mode = True @@ -854,7 +827,8 @@ def run_python_tests(options): print('\n** Running MD on a folder of images (module) **\n') - from megadetector.detection.run_detector_batch import load_and_run_detector_batch,write_results_to_file + from megadetector.detection.run_detector_batch import \ + load_and_run_detector_batch,write_results_to_file results = load_and_run_detector_batch(options.default_model, image_file_names, @@ -872,8 +846,7 @@ def run_python_tests(options): validate_batch_results(inference_output_file) # Verify value correctness - expected_results_file = get_expected_results_filename(is_gpu_available(verbose=False), - options=options) + expected_results_file = get_expected_results_filename(options=options) compare_results(inference_output_file,expected_results_file,options) @@ -905,10 +878,10 @@ def run_python_tests(options): relative_path_base=image_folder, detector_file=options.default_model) - expected_results_file = get_expected_results_filename(is_gpu_available(verbose=False), - options=options) + expected_results_file = get_expected_results_filename(options=options) compare_results(inference_output_file_batch,expected_results_file,options) + ## Run and verify again with augmentation enabled print('\n** Running MD on images with augmentation (module) **\n') @@ -925,8 +898,7 @@ def run_python_tests(options): detector_file=options.default_model) expected_results_file_augmented = \ - get_expected_results_filename(is_gpu_available(verbose=False), - augment=True,options=options) + get_expected_results_filename(augment=True,options=options) compare_results(inference_output_file_augmented,expected_results_file_augmented,options) @@ -1074,17 +1046,19 @@ def run_python_tests(options): from megadetector.detection.process_video import ProcessVideoOptions, process_videos from megadetector.utils.path_utils import insert_before_extension + from megadetector.detection.run_detector import DEFAULT_OUTPUT_CONFIDENCE_THRESHOLD video_options = ProcessVideoOptions() video_options.model_file = options.default_model - video_options.input_video_file = os.path.join(options.scratch_dir, - os.path.dirname(options.test_videos[0])) + video_options.input_video_file = os.path.join(options.scratch_dir,'md-test-images') video_options.output_json_file = os.path.join(options.scratch_dir,'video_folder_output.json') video_options.output_video_file = None video_options.recursive = True video_options.verbose = True - video_options.json_confidence_threshold = 0.05 - video_options.time_sample = 2 + video_options.json_confidence_threshold = DEFAULT_OUTPUT_CONFIDENCE_THRESHOLD + # This needs to match + # video_options.time_sample = 2 + video_options.frame_sample = 10 video_options.detector_options = copy(options.detector_options) _ = process_videos(video_options) @@ -1094,7 +1068,7 @@ def run_python_tests(options): ## Verify results expected_results_file = \ - get_expected_results_filename(is_gpu_available(verbose=False),test_type='video',options=options) + get_expected_results_filename(test_type='video',options=options) assert os.path.isfile(expected_results_file) from copy import deepcopy diff --git a/megadetector/utils/url_utils.py b/megadetector/utils/url_utils.py index bd79e216..31580b9e 100644 --- a/megadetector/utils/url_utils.py +++ b/megadetector/utils/url_utils.py @@ -81,7 +81,7 @@ def download_url(url, Args: url (str): the URL to download destination_filename (str, optional): the target filename; if None, will create - a file in system temp space + a file in system temp space. progress_updater (object or bool, optional): can be "None", "False", "True", or a specific callable object. If None or False, no progress updated will be displayed. If True, a default progress bar will be created. diff --git a/megadetector/visualization/visualization_utils.py b/megadetector/visualization/visualization_utils.py index 20781197..86bed880 100644 --- a/megadetector/visualization/visualization_utils.py +++ b/megadetector/visualization/visualization_utils.py @@ -896,11 +896,15 @@ def _load_font(label_font,label_font_size): """ Internal function for loading a font with error handling """ + font = None try: font = ImageFont.truetype(label_font, label_font_size) except Exception: - print('Warning: could not load font {}'.format(label_font)) + # Only print this warning once in a process + if not getattr(_load_font, "_printed_font_warning", False): + print('Warning: could not load font {}'.format(label_font)) + _load_font._printed_font_warning = True font = None if font is None: try: diff --git a/notebooks/megadetector_colab.ipynb b/notebooks/megadetector_colab.ipynb index 2d84bb75..214c6a8f 100644 --- a/notebooks/megadetector_colab.ipynb +++ b/notebooks/megadetector_colab.ipynb @@ -15,9 +15,11 @@ "\n", "Also see the [MegaDetector guide on GitHub](https://github.com/agentmorris/MegaDetector/blob/main/megadetector.md) and the [MegaDetector Python package documentation](https://megadetector.readthedocs.io).\n", "\n", - "This notebook is designed to load camera trap images that have already been uploaded to Google Drive. If you don't have your own images on Google Drive, this notebook will show you how to download some sample images from [LILA](https://lila.science).\n", + "This notebook is designed to load camera trap images that have already been uploaded to Google Drive. If you don't have your own images on Google Drive, this notebook will download some sample images from [LILA](https://lila.science).\n", "\n", - "MegaDetector output is saved in a .json file whose format is described [here](https://github.com/agentmorris/MegaDetector/tree/main/megadetector/api/batch_processing#batch-processing-api-output-format). The last cell in this notebook will give you some pointers on how users typically work with MegaDetector output." + "MegaDetector output is saved in a .json file in the [MegaDetector output format](https://lila.science/megadetector-output-format). The last cell in this notebook will give you some pointers on how users typically work with MegaDetector output.\n", + "\n", + "Optionally, this notebook will also run [SpeciesNet](https://github.com/google/cameratrapai/) (a species classifier) after running MegaDetector." ] }, { @@ -34,53 +36,73 @@ }, { "cell_type": "markdown", - "metadata": { - "id": "VtNnMxtte0EF" - }, + "metadata": {}, "source": [ - "## Install the MegaDetector Python package\n", - "\n", - "This may take 2-3 minutes. You may be asked to re-start the Colab runtime, that's OK." + "## Constants" ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "id": "EMEkgpy6T0pr" - }, + "metadata": {}, "outputs": [], "source": [ - "pip install megadetector" + "import os\n", + "\n", + "# Leave this at the default if you are on Colab and you want this notebook to download\n", + "# sample images for you. Otherwise, set this to the folder from which the notebook should read\n", + "# your images.\n", + "\n", + "image_folder = \"/content/sample-images\"\n", + "image_folder = \"g:/temp/md-colab-test\"\n", + "\n", + "# When we visualize results, we will write images to this folder\n", + "visualization_folder = '/content/visualized_images'\n", + "visualization_folder = \"g:/temp/md-colab-test-visualization\"\n", + "\n", + "# Which MegaDetector model do you want to run?\n", + "#\n", + "# MDv5a, MDv5b, and MDv1000-redwood are the largest and most accurate MD models as of\n", + "# the time I'm editing this Colab (September 2026).\n", + "model_name = 'MDV5A'\n", + "\n", + "# Create subfolder for MD/SpeciesNet visualization\n", + "visualization_folder_megadetector = os.path.join(visualization_folder,'megadetector')\n", + "visualization_folder_speciesnet = os.path.join(visualization_folder,'speciesnet')\n", + "\n", + "# If you run the \"Download sample images\" cell, this is where it will get the sample images\n", + "sample_dataset_url = \\\n", + " \"https://lilawildlife.blob.core.windows.net/lila-wildlife/idaho-camera-traps/idaho-camera-traps-sample-images.zip\"\n", + "\n", + "# SpeciesNet will (optionally) filter classifications based on a country code. If you want to run SpeciesNet,\n", + "# set this to the three-letter country code for the country where your images come from.\n", + "country = 'USA'\n", + "\n", + "# Choose a location for the output JSON files\n", + "output_file_megadetector = os.path.join(image_folder,'megadetector_results.json')\n", + "output_file_speciesnet = os.path.join(image_folder,'speciesnet_results.json')" ] }, { "cell_type": "markdown", "metadata": { - "id": "JyjEgkCsOsak" + "id": "VtNnMxtte0EF" }, "source": [ - "## Mount Google Drive in Colab\n", - "\n", - "You can skip this cell if you are running this notebook locally, and you don't need to access Google Drive.\n", - "\n", - "You can mount your Google Drive if you have your sample images there, or if want to save the results to your Google Drive. \n", - "\n", - "Once you run the cell below, you will be prompted to authorize Colab to access your Google Drive. Your Google Drive folders will then be mounted under `/content/drive` and can be viewed and navigated in the Files pane in Colab.\n", + "## Install the MegaDetector Python package\n", "\n", - "The method is described in [this Colab code snippet](https://colab.research.google.com/notebooks/io.ipynb#scrollTo=u22w3BFiOveA)." + "This may take 2-3 minutes. You may be asked to re-start the Colab runtime, that's OK." ] }, { "cell_type": "code", "execution_count": null, "metadata": { - "id": "XYsrTTR7eF0r" + "id": "EMEkgpy6T0pr" }, "outputs": [], "source": [ - "from google.colab import drive\n", - "drive.mount('/content/drive')" + "!pip install megadetector" ] }, { @@ -89,9 +111,9 @@ "id": "yM3Dl0Bfe0EM" }, "source": [ - "## Download sample images\n", + "## Download sample images (optional)\n", "\n", - "We install Microsoft's [azcopy](https://docs.microsoft.com/en-us/azure/storage/common/storage-use-azcopy-v10) utility, which we then use to download a few camera trap images from the [Snapshot Serengeti](http://lila.science/datasets/snapshot-serengeti) dataset hosted on [lila.science](http://lila.science). If you are using your own data, you can skip this step, and instead use the cell that follows to set \"LOCAL_DIR\" to the folder that contains your images." + "This cell will download a set of sample images from the [Idaho Camera Traps](https://lila.science/datasets/idaho-camera-traps/) dataset. Skip this if you have your own images." ] }, { @@ -102,43 +124,16 @@ }, "outputs": [], "source": [ - "%%bash\n", + "from megadetector.utils.url_utils import download_url\n", + "from megadetector.utils.path_utils import unzip_file\n", "\n", - "# Download azcopy\n", - "wget -q -O azcopy_linux.tar.gz https://aka.ms/downloadazcopy-v10-linux\n", - "tar -xvzf azcopy_linux.tar.gz --wildcards */azcopy --strip 1b\n", - "rm azcopy_linux.tar.gz\n", - "chmod u+x azcopy\n", + "target_filename = os.path.join(image_folder,sample_dataset_url.split('/')[-1])\n", "\n", - "# Copy a few Snapshot Serengeti images to a local directory\n", - "DATASET_URL = \"https://lilawildlife.blob.core.windows.net/lila-wildlife/snapshotserengeti-unzipped/\"\n", - "SELECTED_FOLDER = \"S1/D05/D05_R4\"\n", - "LOCAL_INPUT_DIR = \"/content/snapshotserengeti\"\n", + "download_url(url=sample_dataset_url,\n", + " destination_filename=target_filename,\n", + " force_download=False)\n", "\n", - "./azcopy cp \"${DATASET_URL}${SELECTED_FOLDER}\" \"${LOCAL_INPUT_DIR}\" --recursive" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## ...or use your own images.\n", - "\n", - "If you didn't run the previous cell, use this cell to point this notebook to your images. You'll need to change a few paths below as well." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# If someone runs this whole notebook, make sure we don't over-write the \"LOCAL_DIR\" variable \n", - "# set in the previous cell.\n", - "try:\n", - " _ = LOCAL_INPUT_DIR\n", - "except:\n", - " LOCAL_INPUT_DIR = '/path/to/your/images' # Or, in Windows: r'c:\\path\\to\\your\\images'" + "unzip_file(target_filename)" ] }, { @@ -151,13 +146,11 @@ "\n", "This step executes the Python script `run_detector_batch.py` from the MegaDetector package. It has three mandatory arguments and one optional:\n", "\n", - "1. A MegaDetector model file (this can be a model name, like \"MDV5A\", or a path to a model file).\n", - "2. A folder containing images. This notebook points to the folder where we just put our Snapshot Serengeti images; if your images were already on Google Drive, replace `[Image_Folder]` with your folder name.\n", + "1. A MegaDetector model file (this can be a model name, like \"MDV5A\" or \"MDv1000-redwood, or it can be a path to a model file).\n", + "2. A folder containing images.\n", "3. The output JSON file location and name.\n", "\n", - "There are actually two variants of MegaDetector v5, called \"v5a\" and \"v5b\". By default this notebook runs MDv5a; change \"MDV5A\" to \"MDV5B\" below to run MDv5b instead.\n", - "\n", - "Both run at the same speed; if you are in a Colab session with a GPU accelerator, you should be able to process around four images per second.\n", + "If you are in a Colab session with a GPU accelerator, and you are using MDv5a, MDv5b, or MDv1000-redwood (the largest and most accurate MegaDetector models), you should be able to process around four images per second.\n", "\n", "Here we are running MegaDetector using python -m to invoke the module as if we were running it at the command line. You can call this directly via Python code as well; documentation for this module is available [here](https://megadetector.readthedocs.io/en/latest/detection.html#module-megadetector.detection.run_detector_batch).\n" ] @@ -170,18 +163,10 @@ }, "outputs": [], "source": [ - "# Make sure the local input folder got set correctly\n", - "import os\n", - "assert os.path.isdir(LOCAL_INPUT_DIR)\n", - "\n", - "# Choose a folder of images to process\n", - "images_dir = LOCAL_INPUT_DIR\n", - "\n", - "# Choose a location for the output JSON file\n", - "output_file_path = '/content/drive/My Drive/snapshotserengeti-test/snapshot-serengeti-megadetector-results.json'\n", + "print('Processing images from {}'.format(image_folder))\n", "\n", "# Run MegaDetector\n", - "!python -m megadetector.detection.run_detector_batch \"MDV5A\" \"$images_dir\" \"$output_file_path\" --recursive --output_relative_filenames --quiet" + "!python -m megadetector.detection.run_detector_batch \"$model_name\" \"$image_folder\" \"$output_file_megadetector\" --recursive --output_relative_filenames --quiet" ] }, { @@ -203,9 +188,7 @@ }, "outputs": [], "source": [ - "# Render bounding boxes on our images\n", - "visualization_dir = '/content/visualized_images'\n", - "!python -m megadetector.visualization.visualize_detector_output \"$output_file_path\" \"$visualization_dir\" --confidence 0.2 --images_dir \"$images_dir\"" + "!python -m megadetector.visualization.visualize_detector_output \"$output_file_megadetector\" \"$visualization_folder_megadetector\" --confidence 0.2 --images_dir \"$image_folder\"" ] }, { @@ -220,9 +203,64 @@ "import os\n", "from PIL import Image\n", "\n", - "for viz_file_name in os.listdir(visualization_dir):\n", + "for viz_file_name in os.listdir(visualization_folder_megadetector):\n", + " print(viz_file_name)\n", + " im = Image.open(os.path.join(visualization_folder_megadetector, viz_file_name))\n", + " display(im)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optional: run SpeciesNet on your images\n", + "\n", + "[SpeciesNet](https://github.com/google/cameratrapai/) is a species classifier that plays nicely with MegaDetector and can be run directly from the MegaDetector Python package. The next cell (optional) will use the MegaDetector results we've already generated to run SpeciesNet." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install the SpeciesNet Python package\n", + "!pip install speciesnet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Run SpeciesNet\n", + "!python -m megadetector.detection.run_md_and_speciesnet \"$image_folder\" \"$output_file_speciesnet\" --detections_file \"$output_file_megadetector\" --country \"$country\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Render bounding boxes on our images, this time with species labels from SpeciesNet\n", + "!python -m megadetector.visualization.visualize_detector_output \"$output_file_speciesnet\" \"$visualization_folder_speciesnet\" --confidence 0.2 --images_dir \"$image_folder\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Show the images with bounding boxes in Colab\n", + "import os\n", + "from PIL import Image\n", + "\n", + "for viz_file_name in os.listdir(visualization_folder_speciesnet):\n", " print(viz_file_name)\n", - " im = Image.open(os.path.join(visualization_dir, viz_file_name))\n", + " im = Image.open(os.path.join(visualization_folder_speciesnet, viz_file_name))\n", " display(im)" ] }, @@ -279,7 +317,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.15" + "version": "3.12.11" } }, "nbformat": 4,