Skip to content

Commit

Permalink
Merge Backend Repo Code (#34)
Browse files Browse the repository at this point in the history
* 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
petargyurov authored Feb 23, 2021
1 parent bff7c57 commit 1ab0736
Show file tree
Hide file tree
Showing 12 changed files with 1,424 additions and 11 deletions.
14 changes: 12 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@
/public/build/
/dist/

.DS_Store
/**/.idea/
/**/__pycache__/
/**/venv/
/**/build/
/**/*.egg-info/

*.pyc
*.pb
*.exe
*.log
*.log
*.spec

.DS_Store
90 changes: 90 additions & 0 deletions engine/README.md
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)
37 changes: 37 additions & 0 deletions engine/annotation_constants.py
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 added engine/cli_wrapper/__init__.py
Empty file.
112 changes: 112 additions & 0 deletions engine/cli_wrapper/cli.py
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:])
8 changes: 8 additions & 0 deletions engine/requirements.txt
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
15 changes: 15 additions & 0 deletions engine/setup.py
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
''',
)
Loading

0 comments on commit 1ab0736

Please sign in to comment.