-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Initial commit of backend code in this repo * Updated .exe path * Updated .exe path for installer and removed unused commands * Ensuring we are not including backend source code into the final build
- Loading branch information
1 parent
bff7c57
commit 1ab0736
Showing
12 changed files
with
1,424 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
# Megadetector API | ||
|
||
At its core, this repository is a fork of the Megadetector project that sits within | ||
the [microsoft/CameraTraps](https://github.com/microsoft/CameraTraps) repository. | ||
The aim is to expose a simpler API to get started with the model, for both | ||
programmers and non-programmers. | ||
|
||
The original CameraTraps repository hosts a number of projects and tools and is | ||
difficult to navigate. In addition, the code provided for running the | ||
Megadetector model is designed specifically as a CLI tool, making it difficult | ||
to use as a script in a pipeline or to build up on and extend. | ||
|
||
### Basic Usage | ||
|
||
```python | ||
from tf_detector import TFDetector | ||
|
||
tf_detector = TFDetector(model_path='md_v4.1.0.pb', output_path='/output') | ||
|
||
results = tf_detector.run_detection(input_path='test_imgs/stoats') | ||
|
||
``` | ||
|
||
##### Example of `results.json` | ||
```json | ||
[ | ||
{ | ||
"file": "my_images/img1.JPG", | ||
"max_detection_conf": 0.981, | ||
"detections": [ | ||
{ | ||
"category": "1", | ||
"conf": 0.981, | ||
"bbox": [ | ||
0.4011, | ||
0.6796, | ||
0.07269, | ||
0.07873 | ||
] | ||
} | ||
] | ||
}, | ||
... | ||
] | ||
``` | ||
|
||
|
||
### Features | ||
|
||
- ✔️Simple API to expose the model and run it on images | ||
- ✔️Structural refactoring and code improvements | ||
- ✔️CLI interface | ||
- ✔️For GUI see [megadetector-gui](https://github.com/petargyurov/megadetector-gui) which is powered by this repo | ||
- **TODO:** add basic evaluation metrics to output | ||
- **TODO:** add proper logging functionality | ||
- **TODO:** add different export formats (e.g.: `.csv`) | ||
|
||
|
||
### What's changed from the original? | ||
|
||
For the most part, the functionality of the base `TFDetector` class remains | ||
unchanged. | ||
|
||
- abstracted the main class, `TFDetector`, from (almost) any CLI functionality | ||
- the class has its own method to start detections | ||
- class instances can now be initialised with threshold params, amongst other things | ||
- refactored the class`ImagePathUtils` into a simple `utils.py` module; | ||
there was no reason to have it as a class | ||
- moved some additional methods to `utils.py` | ||
|
||
### Building An Executable | ||
|
||
To distribute this program you can build an executable. This is particularly handy | ||
for use with [megadetector-gui](https://github.com/petargyurov/megadetector-gui). | ||
|
||
Steps: | ||
1. Clone this repo | ||
2. Create a virtual environment | ||
3. Activate the virtual environment | ||
4. Install requirements: `pip install -r requirements.txt` | ||
5. Download the MegaDetector model from [here](https://github.com/microsoft/CameraTraps/blob/master/megadetector.md#download-links) | ||
5. Build the .exe: `pyinstaller -F cli_wrapper/cli.py` | ||
|
||
The build process will take a bit of time. Inside the newly created `dist/` folder | ||
you will find `cli.exe` which can be distributed. | ||
|
||
If you plan on using the CLI often, put the `cli.exe` file somewhere more | ||
permanent (e.g.: `C:\Users\<user>\`) and add it to your `PATH` environment variable | ||
so that you can invoke it from anywhere. You still need to specify the path | ||
of the model file each time (something that might be made simpler in the future) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
""" | ||
Taken from https://github.com/microsoft/CameraTraps/blob/9274b2b2c3b7675341c9d065d1782fc74c428943/data_management/annotations/annotation_constants.py | ||
""" | ||
|
||
NUM_DETECTOR_CATEGORIES = 3 # this is for choosing colors, so ignoring the "empty" class | ||
|
||
# This is the label mapping used for our incoming iMerit annotations | ||
# Only used to parse the incoming annotations. In our database, the string name is used to avoid confusion | ||
annotation_bbox_categories = [ | ||
{'id': 0, 'name': 'empty'}, | ||
{'id': 1, 'name': 'animal'}, | ||
{'id': 2, 'name': 'person'}, | ||
{'id': 3, 'name': 'group'}, # group of animals | ||
{'id': 4, 'name': 'vehicle'} | ||
] | ||
|
||
annotation_bbox_category_id_to_name = {} | ||
annotation_bbox_category_name_to_id = {} | ||
|
||
for cat in annotation_bbox_categories: | ||
annotation_bbox_category_id_to_name[cat['id']] = cat['name'] | ||
annotation_bbox_category_name_to_id[cat['name']] = cat['id'] | ||
|
||
# MegaDetector outputs | ||
detector_bbox_categories = [ | ||
{'id': 0, 'name': 'empty'}, | ||
{'id': 1, 'name': 'animal'}, | ||
{'id': 2, 'name': 'person'}, | ||
{'id': 3, 'name': 'vehicle'} | ||
] | ||
|
||
detector_bbox_category_id_to_name = {} | ||
detector_bbox_category_name_to_id = {} | ||
|
||
for cat in detector_bbox_categories: | ||
detector_bbox_category_id_to_name[cat['id']] = cat['name'] | ||
detector_bbox_category_name_to_id[cat['name']] = cat['id'] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import sys | ||
import os | ||
import json | ||
import click | ||
|
||
from utils import save_as_csv | ||
|
||
|
||
@click.group() | ||
def mega(): | ||
pass | ||
|
||
|
||
# TODO: support for results field | ||
|
||
|
||
@mega.command() | ||
@click.argument('model-path') | ||
@click.argument('input-path') | ||
@click.argument('output-path') | ||
@click.option('-rcf', '--round-conf', default=3, help='Number of decimal places to round confidence to') | ||
@click.option('-rcd', '--round-coord', default=4, help='Number of decimal places to round bbox coordinates to') | ||
@click.option('-rt', '--render-thresh', default=0.85, help='Minimum confidence value required to render a bbox') | ||
@click.option('-ot', '--output-thresh', default=0.1, help='Minimum confidence value required to output a detection in the output file') | ||
@click.option('--recursive/--not-recursive', default=False, help='Whether to search for images in folders within the base folder provided') | ||
@click.option('-n', '--n-cores', default=0, help='Number of CPU cores to utilise. Will be ignored if a valid GPU is available') | ||
@click.option('-cp', '--checkpoint-path', default=None, type=str, help='Path to JSON checkpoint file') | ||
@click.option('-cf', '--checkpoint-frequency', default=-1, type=str, help='How often to write to checkpoint file, i.e.: every N images') | ||
@click.option('--show/--no-show', default=False, help='Whether to output the results in the console') | ||
@click.option('--bbox/--no-bbox', default=True, help='Whether save images with bounding boxes.') | ||
@click.option('--verbose/--quiet', default=False, help='Whether to output or supress Tensorflow message') | ||
@click.option('--electron/--no-electron', default=False, help='Whether we\'re calling this from Electron; stdout is handled differently') | ||
@click.option('--auto-sort/--no-auto-sort', default=False, help='Whether to automatically move original images into categorised folders') | ||
def detect(model_path, input_path, output_path, round_conf, round_coord, render_thresh, | ||
output_thresh, recursive, n_cores, checkpoint_path, | ||
checkpoint_frequency, show, bbox, verbose, electron, auto_sort): | ||
"""Runs detection procedure on a set of images using a given | ||
MegaDetector model. | ||
MODEL_PATH: the path of the MegaDetector model file to use | ||
INPUT_PATH: the path of the image folder | ||
OUTPUT_PATH: path in which to save bbox images and JSON summary. | ||
""" | ||
|
||
if not verbose: | ||
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' | ||
|
||
from tf_detector import TFDetector | ||
|
||
tf_detector = TFDetector(model_path=model_path, | ||
output_path=output_path, | ||
conf_digits=round_conf, | ||
coord_digits=round_coord, | ||
render_conf_threshold=render_thresh, | ||
output_conf_threshold=output_thresh) | ||
|
||
results = tf_detector.run_detection(input_path=input_path, | ||
generate_bbox_images=bbox, | ||
recursive=recursive, | ||
n_cores=n_cores, | ||
results=None, | ||
checkpoint_path=checkpoint_path, | ||
checkpoint_frequency=checkpoint_frequency, | ||
electron=electron) | ||
|
||
if auto_sort: | ||
mega(["move", os.path.join(tf_detector.output_path, 'results.json'), "--auto-sort"]) | ||
|
||
if show: | ||
click.echo_via_pager( | ||
json.dumps(r, indent=4, default=str) for r in results) | ||
|
||
|
||
@mega.command() | ||
@click.argument('results_path') | ||
@click.option('--auto-sort/--no-auto-sort', default=False, help='Whether to automatically move original images into categorised folders') | ||
def move(results_path, auto_sort): | ||
with open(results_path) as f: | ||
results = json.load(f) | ||
|
||
images = results['images'] | ||
categories = results['detection_categories'] | ||
|
||
try: | ||
save_as_csv(images) # TODO: this really doesn't belong here | ||
except Exception: | ||
pass | ||
|
||
for img in images: | ||
if not img.get('reviewed') and not auto_sort: # skip images that haven't been reviewed | ||
continue | ||
category = img['detections'][0]['category'] if img['detections'] else 0 | ||
category = categories.get(category, 'empty') | ||
dest = os.path.join(os.path.dirname(img['file']), category) | ||
|
||
if not os.path.exists(dest): | ||
os.makedirs(dest) | ||
|
||
try: | ||
# move image | ||
os.rename(img['file'], os.path.join(dest, os.path.basename(img['file']))) | ||
except FileNotFoundError: | ||
continue | ||
|
||
|
||
if getattr(sys, 'frozen', False): | ||
mega(sys.argv[1:]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
numpy==1.18.5 | ||
Pillow==8.0.1 | ||
requests==2.25.0 | ||
tensorflow==2.3.1 | ||
tensorflow-gpu~=2.3.1 | ||
setuptools~=50.3.2 | ||
click~=7.1.2 | ||
pyinstaller |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
from setuptools import find_packages, setup | ||
|
||
setup( | ||
name='cli_wrapper', | ||
version='0.1', | ||
packages=find_packages(), | ||
include_package_data=True, | ||
install_requires=[ | ||
'Click', | ||
], | ||
entry_points=''' | ||
[console_scripts] | ||
mega=cli_wrapper.cli:mega | ||
''', | ||
) |
Oops, something went wrong.