From 89effd99e312862100675d377d70b626b13ce094 Mon Sep 17 00:00:00 2001 From: Katie Garwood <115747639+kgarwoodsdzwa@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:17:28 -0800 Subject: [PATCH 01/12] Revert "updating wild demo to reflect master" --- README.md | 80 ++++++--------------- config/fetch_and_alert.yml | 35 ++++----- cougarvision_utils/detect_img.py | 47 ++++++++---- cougarvision_utils/strikeforceget.py | 19 ----- cougarvision_utils/strikeforcegetcameras.py | 36 ---------- fetch_and_alert.py | 13 ++-- 6 files changed, 81 insertions(+), 149 deletions(-) delete mode 100644 cougarvision_utils/strikeforceget.py delete mode 100644 cougarvision_utils/strikeforcegetcameras.py diff --git a/README.md b/README.md index 8fb60f0..ab1e806 100644 --- a/README.md +++ b/README.md @@ -10,21 +10,27 @@ program. Its name is an homage both to the species for which it was originally developed and the mascot of Cal State San Marcos where Jared was then studying, which is also the cougar. -# Setting up Conda Environment -First, you must install conda (whatever the latest version of Anaconda is) on your machine, install instructions can be found here: +## Streaming +Stream_detect.py takes in a stream, runs a pipeline to detect various animals. An image is sent to specified emails/phone numbers based on config/stream_detect.yml. +If a lizard or a cougar is detected then a video is recorded for as long as that animal remains to be detected. -[Instructions to install conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html) +## Fetch and alert +Fetch_and_alert.py combines two functions to retrieve images from [Strikeforce](https://www.strikeforcewireless.com) site and email addresses. A webscrapper is used to retrieve images from Strikeforce username and password can be changed in the config/fetch_and_alert.yml file. Emails are accessed and images attached are extracted, email addresses may also be changed in the same config file. Once extracted these images +are ran through both detector and classification models. And alerts (sends emails/texts) if a cougar is detected. +To run fetch_and_alert.py, the config/fetch_and_alert.yml must be configured according to the notes in the file. The command line script to run is: -If you haven't done so already: ``` -sudo apt install git -``` -Now you should be ready to clone the repository: -``` -git clone https://github.com/conservationtechlab/cougarvision.git +python3 fetch_and_alert.py config/fetch_and_alert.yml -cd cougarvision ``` + +## Processing Batch Images +Run_batch_images.py can take in an input folder and apply the detector/classifier pipeline to render annotated images along with an output.json file in directory classifications. The output.json contains the final classifications on the cropped images and can be used to compare against a ground truth set. There is also an intermediate output.json which holds the crop detections and is used by the script to crop images, this one can be moved by configuration in the yml file. + +## Setting up Conda Environment + +[Instructions to install conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html) + The file **cougarvision_env.yml** describes the python version and various dependencies with specific version numbers. To activate the enviroment ``` @@ -33,63 +39,21 @@ conda env create -f cougarvision_env.yml conda activate cougarvision_env conda env list + ``` The first line creates the enviroment from the specifications file which only needs to be done once. The second line activates the environment which may need to be done each time you restart the terminal. -The third line is to test the environment installation where it will list all dependencies for that environment. - -The environment must be activated anytime you wish to run the scripts within this package. - -# Models +The third line is to test the environment installation where it will list all dependencies for that environment -Detection and inference models can be placed anywhere. In each script's yml file (under the config directory) is a field where the path variables for each model can be specified. Linked are the current updated models along with their class lists for [Peru](https://sandiegozoo.box.com/s/jfw7ih8xedzsn83to91pg6gvaq1nj5bl),[Peru class list](https://sandiegozoo.box.com/s/xng8erxrvw6nz98h8xjtk8avnopfz6ev), [Southwest](https://sandiegozoo.box.com/s/x63lnaxw8hag39mczeommqy9tw4t0ht9), [Southwest class list](https://sandiegozoo.box.com/s/hn8nput5pxjc3toao57gfn4h6zo1lyng) and [Kenya](https://sandiegozoo.box.com/s/cwn5wss9gjibvf57xop2zgfmlih512lt), [Kenya class list](https://sandiegozoo.box.com/s/f5athitml7bedix0ubnccyg8npvsr6ip). The detection model that is currently integrated with fetch_and_alert.py functionality is [MDv5 using pytorch](https://github.com/ecologize/CameraTraps/releases/download/v5.0/md_v5a.0.0.pt) +## Dependencies -# Fetch and alert -Fetch_and_alert.py combines two functions to retrieve images from [Strikeforce](https://www.strikeforcewireless.com) site and email addresses. Strikeforce username and password can be changed in the config/fetch_and_alert.yml file. Emails are accessed and images attached are extracted, email addresses may also be changed in the same config file. Once extracted these images are ran through both detector and classification models. And alerts (sends emails/texts/EarthRanger event) if a cougar (or any target animal you define in config as long as they are an option on the class list for your classification model) is detected. -To run fetch_and_alert.py, the config/fetch_and_alert.yml must be configured according to the notes in the file. The command line script to run is: - -``` -python3 fetch_and_alert.py config/fetch_and_alert.yml - -``` -## Installing animl -``` -pip3 install animl -``` -## Obtaining Strikeforce camera dictionary/auth_token -In order to retrieve photos from strikeforce, you need the api auth token for your account. We currently have a python script within /cougarvision_utils/strikeforceget.py that can obtain this auth_token given the strikeforce account username and password. -Once you have this token, place it in 'auth_token' within the config file. -To obtain the unique camera IDs in Strikeforce, run the script within /cougarvision_utils/strikeforcegetcameras.py with your email and the auth_token obtained from strikekforceget.py and it will output the unique strikeforce ID along with your camera names. -``` -python3 strikeforceget.py -python3 strikeforcegetcameras.py -``` -In this version of CougarVision, all camera names need to be 4 characters long, so if your camera names exceed this length, shorten them to 4 characters within the config directory. - -## EarthRanger integration -CougarVision can optionally send detections for a species of interest to their specific camera location in EarthRanger. -For this functionality, the er_alerts config must be set to "True". -You also need to include an authorization token from EarthRanger. This can be obtained from .pamdas.org/api/v1.0/docs/interactive/. If you run the first example (das_server) get>try it out>execute, you will see a 'Curl' example input. There will be an X-CSRF and Authorization token, copy and paste these values into 'token' and 'authorization' in the config file, respectively (including 'Bearer ' for the authorization). These tokens will expire relatively quickly. To make them last longer, head on over to .pamdas.org/admin under 'Django OAuth Toolkit'>access tokens and paste the authorization token into the search bar(not including 'Bearer '). Click on the option that matches, and change the expiration date for however far out you'd like it to expire. It's important to note that you should keep this token secret, especially if it will not expire for a while. - -In order for this integration to work, the camera name in EarthRanger must be the same as the 4 digit name in the camera dictionary in the config file. For ease of adding your cameras in the same format we have, there is a script in our [EarthRanger integration API package](https://github.com/conservationtechlab/sageranger/blob/main/sageranger/post_camera_er.py) that will add the cameras correctly. Verify that the cameras are visible on your EarthRanger map instance before proceeding with this integration. +These scripts rely on the [CameraTraps Repo](https://github.com/microsoft/CameraTraps). Download this repo and place it anywhere. Add that path to config/cameratraps.yml. The CameraTraps repo will then be added to the python sys environment variables upon loading an individual script. -## Email alerts -In order to send email alerts, CougarVision needs to know from what email to send them. For this, any email will do, but you need to include the email and password to the email. We just created a gmail specifically for this purpose, and include the email and the password under 'username' and 'password' in the config file. +## Models -## Additional setup -Create two directories anywhere, one called 'images' and one called 'logs'. Place their file paths in the config under 'save_dir' and 'log_dir'. This will be where all photos from strikeforce are stored locally and where all the dataframes containing detection information for each photo will live. +Detection and inference models can be place anywhere. In each script's yml file (under the config directory) is a field where the path variables for each model can be specified. -# Streaming -Stream_detect.py takes in a stream, runs a pipeline to detect various animals. An image is sent to specified emails/phone numbers based on config/stream_detect.yml. -If a lizard or a cougar is detected then a video is recorded for as long as that animal remains to be detected. -These scripts rely on the [CameraTraps Repo](https://github.com/microsoft/CameraTraps). Download this repo and place it anywhere. Add that path to config. The CameraTraps repo will then be added to the python sys environment variables upon loading an individual script. - -# Processing Batch Images -Run_batch_images.py can take in an input folder and apply the detector/classifier pipeline to render annotated images along with an output.json file in directory classifications. The output.json contains the final classifications on the cropped images and can be used to compare against a ground truth set. There is also an intermediate output.json which holds the crop detections and is used by the script to crop images, this one can be moved by configuration in the yml file. -These scripts rely on the [CameraTraps Repo](https://github.com/microsoft/CameraTraps). Download this repo and place it anywhere. Add that path to config. The CameraTraps repo will then be added to the python sys environment variables upon loading an individual script. -# Improvement Efforts -If you encounter any issues upon install or would like a feature added, create an issue in the issue tracker on this github and we will do our best to accomodate! Specifically we are looking to add new integrations for other types of cellular camera traps. diff --git a/config/fetch_and_alert.yml b/config/fetch_and_alert.yml index a414ff8..5888861 100644 --- a/config/fetch_and_alert.yml +++ b/config/fetch_and_alert.yml @@ -1,9 +1,9 @@ #home_dir - path to local cougarvision repo home_dir: /home/johnsmith/cougarvision -email_alerts: True -er_alerts: True +# 1 = email alerts and 2 = earthranger alerts +use_variation: 2 #detector_model - path to detector model -detector_model: /home/johnsmith/cougarvision/detector_models/megadetector.pt +detector_model: /home/johnsmith/cougarvision/detector_models/megadetector_v4.1.pb #classifier_model - path to classifier model classifier_model: /home/johnsmith/cougarvision/classifier_models/EfficientNetB5_456_Unfrozen_05_0.26_0.92.h5 checkpoint_frequency: -1 @@ -11,29 +11,30 @@ checkpoint_frequency: -1 log_dir: /home/johnsmith/cougarvision/logs/ #classes - path to the class list for the classifier model classes: /home/johnsmith/cougarvision/labels/sw_classes.txt -#the emails that will receive email alerts, can be multiple emails -consumer_emails: [] -# the amiul (s) you with to receive the developmemt email alert- contains confidence value of detection -dev_emails: [] +#to_email - the email that will receive email alerts, can be multiple emails +to_emails: [your@email.com] #threshold confidence score -confidence: 0.7 +confidence: 0.01 #cpu threads threads: 8 # Web Scraping #strike force api url strikeforce_api: https://api.strikeforcewireless.com/api/v2/ #strikeforce wireless username -username_scraper: [yourusername.cam@gmail.com] +username_scraper: yourusername.cam@gmail.com #strikeforce wireless password -password_scraper: [yourpassword] -#authorization token from Strike Force, can be obtained with login credentials and using strikeforceget.py in /cougarvision_utils folder -auth_token: [""] +password_scraper: yourpassword +#authorization token from Strike Force +auth_token: #save_dir - path to where the images get stored (must create folder) save_dir: /home/johnsmith/images/ -#dictionary containing the strikeforce unique ID with the actual 4 digit camera name, ID can be obtained using strikeforcegetcameras.py within the /cougarvision_utils folder -camera_names: {'': , ...} +#dict of camera names and their respective strikeforce wireless ids, these must be retrieved from +#strikeforce wireless web dev tools in the code and currently the cam names must be 4 chars long +camera_names: {'62201': B018, '59760': B016, '59681': B013, '59758': B015, '60095': B014, + '60272': B017, '85767': B041, '105353': B044, '105355': B045, '105357': B046, '105359': B047, + '105360': B048, '104322': B049} #animals that the detector will send alerts for if detected (from class list) -alert_targets: [cougar, bobcat, skunk, deer, dog, coyote] +alert_targets: [cougar, bobcat, skunk, deer, dog, coyote, rabbit, elephant] #checkn_interval - time interval between still-alive emails to let us know no errors have crashed #cougarvision if it's running constantly checkin_interval: 24 @@ -41,6 +42,6 @@ checkin_interval: 24 admin: admin@email.com #x-csrf token retreived from earthranger interactive api example requests, must be logged into an #existing earthranger account with interactive api access -token: '' +token: [insert token] #authorization bearer token retreived from same earthranger interactive api example requests -authorization: 'Bearer ' +authorization: Bearer [insert authorization] diff --git a/cougarvision_utils/detect_img.py b/cougarvision_utils/detect_img.py index e85e96a..8cb41ab 100644 --- a/cougarvision_utils/detect_img.py +++ b/cougarvision_utils/detect_img.py @@ -13,10 +13,11 @@ from datetime import datetime as dt import time import re -import sys import yaml +import sys +import yolov5 from PIL import Image -from animl import parse_results, classify, split +from animl import parseResults, imageCropGenerator, splitData, detectMD from sageranger import is_target, attach_image, post_event from animl.detectMD import detect_MD_batch import os @@ -27,8 +28,18 @@ with open("config/cameratraps.yml", 'r') as stream: - CAM_CONFIG = yaml.safe_load(stream) - sys.path.append(CAM_CONFIG['camera_traps_path']) + camera_traps_config = yaml.safe_load(stream) + sys.path.append(camera_traps_config['camera_traps_path']) + + +def get_last_file_number(folder_path): + max_num = 0 + for filename in os.listdir(folder_path): + # Extract digits from the filename using regex + num = re.findall(r'\d+', filename) + if num: # If there are digits in the filename + max_num = max(max_num, int(num[-1])) # Use the last set of digits as the number + return max_num def detect(images, config, c_model, d_model): @@ -44,6 +55,7 @@ def detect(images, config, c_model, d_model): config: the unpacked config values from fetch_and_alert.yml that contains necessary parameters the function needs ''' + #use_variation = int(config['use_variation']) email_alerts = bool(config['email_alerts']) er_alerts = bool(config['er_alerts']) log_dir = config['log_dir'] @@ -70,27 +82,38 @@ def detect(images, config, c_model, d_model): confidence_threshold=confidence, checkpoint_frequency=checkpoint_f, results=None, + n_cores=1, quiet=False, image_size=None) end = time.time() md_time = end - start logging.debug('Time to detect: ' + str(md_time)) # Parse results - data_frame = parse_results.from_MD(results, None, None) + data_frame = parseResults.parseMD(results, None, None) # filter out all non animal detections if not data_frame.empty: - animal_df = split.getAnimals(data_frame) - other_df = split.getEmpty(data_frame) + animal_df = splitData.getAnimals(data_frame) + otherdf = splitData.getEmpty(data_frame) # run classifier on animal detections if there are any if not animal_df.empty: # create generator for images - predictions = classify.predict_species(animal_df, c_model, - batch=4) + + generator = imageCropGenerator.\ + GenerateCropsFromFile(animal_df) # changed function + # Run Classifier + start = time.time() + predictions = c_model.predict_generator(generator, + steps=len(generator), + verbose=1) + end = time.time() + cls_time = end - start + logging.debug('Time to classify: ' + str(cls_time)) # Parse results - max_df = parse_results.from_classifier(animal_df, + max_df = parseResults.applyPredictions(animal_df, predictions, classes, - None) + None, + False) # Creates a data frame with all relevant data cougars = max_df[max_df['prediction'].isin(targets)] # drops all detections with confidence less than threshold @@ -105,7 +128,7 @@ def detect(images, config, c_model, d_model): label = cougars.at[idx, 'prediction'] # uncomment this line to use conf value for dev email alert prob = str(cougars.at[idx, 'conf']) - label = cougars.at[idx, 'class'] + #label = cougars.at[idx, 'class'] img = Image.open(cougars.at[idx, 'file']) draw_bounding_box_on_image(img, cougars.at[idx, 'bbox2'], diff --git a/cougarvision_utils/strikeforceget.py b/cougarvision_utils/strikeforceget.py deleted file mode 100644 index f1628d5..0000000 --- a/cougarvision_utils/strikeforceget.py +++ /dev/null @@ -1,19 +0,0 @@ -'''Script for getting strikeforce auth_token''' -import requests -import json - -username = "" -password = "" - - -base = "https://api.strikeforcewireless.com/api/v2/" -request = "users/sign-in/" -call = base + request -body = json.dumps({"user": {"email": username, "password": password}}) -encode = 'json' -response = requests.post(url=call, data=body, - headers={"Content-Type": "application/json"}) -response = response.text -response = json.loads(response) -authentication_token = response["meta"]["authentication_token"] -print(authentication_token) diff --git a/cougarvision_utils/strikeforcegetcameras.py b/cougarvision_utils/strikeforcegetcameras.py deleted file mode 100644 index 82c87cd..0000000 --- a/cougarvision_utils/strikeforcegetcameras.py +++ /dev/null @@ -1,36 +0,0 @@ -'''Script for obtaining strikeforce camera IDs''' -import requests -import json - - -def getData(base, request, parameters, username, authentication_token): - '''Function for retrieving camera info from strikeforce - Args: - base: strikeforce api link - request: cameras - parameters: none - username: strikeforce username email - authentication_token: auth token obtained from strikeforceget.py - - Returns: - json with all camera info''' - call = base + request + "?" + parameters - headers = {"X-User-Email": username, "X-User-Token": authentication_token} - response = requests.get(call, headers=headers) - data = response.text - return json.loads(data) - - -base = "https://api.strikeforcewireless.com/api/v2/" -request = "cameras" -parameters = "" -username = "" -authentication_token = "" - -data = getData(base, request, parameters, username, authentication_token) -pretty_json = json.dumps(data, indent=4) -cameras = [] -list_of_cam_info = data['data'] -for i in range(len(list_of_cam_info)): - cameras = list_of_cam_info[i]['id'] - print(cameras) diff --git a/fetch_and_alert.py b/fetch_and_alert.py index 4b27e69..ac44110 100644 --- a/fetch_and_alert.py +++ b/fetch_and_alert.py @@ -22,16 +22,16 @@ import time import warnings from datetime import datetime as dt -import logging import yaml import schedule +import logging from cougarvision_utils.detect_img import detect from cougarvision_utils.alert import checkin from cougarvision_utils.get_images import fetch_image_api -from sageranger.post_monthly import post_monthly_obs -from animl.classify import load_classifier -from animl import megadetector +#from sageranger.post_monthly import post_monthly_obs +from animl.predictSpecies import load_classifier +from animl.detectMD import load_MD_model # Numpy FutureWarnings from tensorflow import @@ -62,12 +62,11 @@ # load models once CLASSIFIER_MODEL = load_classifier(CLASSIFIER) -DETECTOR_MODEL = megadetector.MegaDetector(DETECTOR) +DETECTOR_MODEL = load_MD_model(DETECTOR) def logger(): - '''Function for creating log file''' - logging.basicConfig(filename='cougarvision.log', level=logging.INFO) + logging.basicConfig(filename='cougarvision.log', format='%(levelname)s:%(asctime)s:%(module)s:%(funcName)s: %(message)s', level=logging.DEBUG) def fetch_detect_alert(): From 11906efcd5cac2c17161cf2b3dcbbbe22de7b5e9 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Tue, 21 Nov 2023 16:21:19 -0800 Subject: [PATCH 02/12] Make the demo modular The demo branch was created hard and fast and these commits make the demo portion more modular in the sense that with merely a few extra lines to the config, you can make fetch_and_alert run the necessary portions to allow the output to be visualized. Currently, one must run this version of fetch_and_alert and simultaneously run display.py with the same config file as fetch and alert to work. display.py is the main function and visualize_helper.py contain some functions that get called in fetch_and_alert to ensure the images are in the proper place and format for display.py to reach them. --- config/fetch_and_alert.yml | 6 +- cougarvision_utils/detect_img.py | 62 ++++----- cougarvision_utils/get_images.py | 45 +++---- cougarvision_visualize/display.py | 150 +++++++++++++++++++++ cougarvision_visualize/visualize_helper.py | 25 ++++ demo2.py | 117 ---------------- fetch_and_alert.py | 19 ++- 7 files changed, 235 insertions(+), 189 deletions(-) create mode 100644 cougarvision_visualize/display.py create mode 100644 cougarvision_visualize/visualize_helper.py delete mode 100644 demo2.py diff --git a/config/fetch_and_alert.yml b/config/fetch_and_alert.yml index 5888861..31ace9c 100644 --- a/config/fetch_and_alert.yml +++ b/config/fetch_and_alert.yml @@ -12,9 +12,13 @@ log_dir: /home/johnsmith/cougarvision/logs/ #classes - path to the class list for the classifier model classes: /home/johnsmith/cougarvision/labels/sw_classes.txt #to_email - the email that will receive email alerts, can be multiple emails +#Run version that allows for images cast to two screens? True/False +visualize_output: True +path_to_unlabeled_output: /path/to/unlabeled/img/folder +path_to_labeled_output: /path/to/labeled/img/folder to_emails: [your@email.com] #threshold confidence score -confidence: 0.01 +confidence: 0.7 #cpu threads threads: 8 # Web Scraping diff --git a/cougarvision_utils/detect_img.py b/cougarvision_utils/detect_img.py index 8cb41ab..25a62b7 100644 --- a/cougarvision_utils/detect_img.py +++ b/cougarvision_utils/detect_img.py @@ -13,33 +13,23 @@ from datetime import datetime as dt import time import re -import yaml import sys -import yolov5 +import logging +import yaml from PIL import Image -from animl import parseResults, imageCropGenerator, splitData, detectMD +from animl import parse_results, classify, split from sageranger import is_target, attach_image, post_event from animl.detectMD import detect_MD_batch -import os -import logging from cougarvision_utils.cropping import draw_bounding_box_on_image from cougarvision_utils.alert import smtp_setup, send_alert +from cougarvision_visualize.visualize_output import get_last_file_number +from cougarvision_visualize.visualize_output import create_folder with open("config/cameratraps.yml", 'r') as stream: camera_traps_config = yaml.safe_load(stream) sys.path.append(camera_traps_config['camera_traps_path']) - - -def get_last_file_number(folder_path): - max_num = 0 - for filename in os.listdir(folder_path): - # Extract digits from the filename using regex - num = re.findall(r'\d+', filename) - if num: # If there are digits in the filename - max_num = max(max_num, int(num[-1])) # Use the last set of digits as the number - return max_num def detect(images, config, c_model, d_model): @@ -55,7 +45,7 @@ def detect(images, config, c_model, d_model): config: the unpacked config values from fetch_and_alert.yml that contains necessary parameters the function needs ''' - #use_variation = int(config['use_variation']) + # use_variation = int(config['use_variation']) email_alerts = bool(config['email_alerts']) er_alerts = bool(config['er_alerts']) log_dir = config['log_dir'] @@ -71,6 +61,8 @@ def detect(images, config, c_model, d_model): token = config['token'] authorization = config['authorization'] color = config['color'] + visualize_output = config['visualize_output'] + labeled_img = config['path_to_labeled_output'] if len(images) > 0: # extract paths from dataframe image_paths = images[2] @@ -82,34 +74,28 @@ def detect(images, config, c_model, d_model): confidence_threshold=confidence, checkpoint_frequency=checkpoint_f, results=None, - n_cores=1, quiet=False, image_size=None) end = time.time() md_time = end - start logging.debug('Time to detect: ' + str(md_time)) # Parse results - data_frame = parseResults.parseMD(results, None, None) + data_frame = parse_results.from_MD(results, None, None) # filter out all non animal detections if not data_frame.empty: - animal_df = splitData.getAnimals(data_frame) - otherdf = splitData.getEmpty(data_frame) + animal_df = split.getAnimals(data_frame) + otherdf = split.getEmpty(data_frame) # run classifier on animal detections if there are any if not animal_df.empty: # create generator for images - - generator = imageCropGenerator.\ - GenerateCropsFromFile(animal_df) # changed function - # Run Classifier start = time.time() - predictions = c_model.predict_generator(generator, - steps=len(generator), - verbose=1) + predictions = classify.predict_species(animal_df, c_model, + batch=4) end = time.time() cls_time = end - start logging.debug('Time to classify: ' + str(cls_time)) # Parse results - max_df = parseResults.applyPredictions(animal_df, + max_df = parse_results.from_classifier(animal_df, predictions, classes, None, @@ -128,7 +114,7 @@ def detect(images, config, c_model, d_model): label = cougars.at[idx, 'prediction'] # uncomment this line to use conf value for dev email alert prob = str(cougars.at[idx, 'conf']) - #label = cougars.at[idx, 'class'] + label = cougars.at[idx, 'class'] img = Image.open(cougars.at[idx, 'file']) draw_bounding_box_on_image(img, cougars.at[idx, 'bbox2'], @@ -147,15 +133,15 @@ def detect(images, config, c_model, d_model): image_bytes = BytesIO() img.save(image_bytes, format="JPEG") img_byte = image_bytes.getvalue() - - folder_path = '/home/katiedemo/demo_images' - last_file_number = get_last_file_number(folder_path) - new_file_number = last_file_number + 1 - new_file_name = f"{folder_path}/image_{new_file_number}.jpg" + if visualize_output is True: + folder_path = create_folder(labeled_img) + last_file_number = get_last_file_number(folder_path) + new_file_number = last_file_number + 1 + new_file_name = f"{folder_path}/image_{new_file_number}.jpg" + + with open(new_file_name, "wb") as folder: + folder.write(img_byte) - with open(new_file_name, "wb") as f: - f.write(img_byte) - cam_name = cougars.at[idx, 'cam_name'] if label in targets and er_alerts is True: is_target(cam_name, token, authorization, label) @@ -172,7 +158,7 @@ def detect(images, config, c_model, d_model): authorization, label) logging.info(response) - + logging.info('Sending detection email') if email_alerts is True: smtp_server = smtp_setup(username, password, host) diff --git a/cougarvision_utils/get_images.py b/cougarvision_utils/get_images.py index 37787b0..d772097 100644 --- a/cougarvision_utils/get_images.py +++ b/cougarvision_utils/get_images.py @@ -15,10 +15,11 @@ import json import urllib.request import os.path +import logging import requests import numpy as np -import logging -import re +from cougarvision_visualize.visualize_output import get_last_file_number +from cougarvision_visualize.visualize_output import create_folder ''' @@ -52,17 +53,6 @@ ''' -def get_last_file_number(folder_path): - max_num = 0 - for filename in os.listdir(folder_path): - # Extract digits from the filename using regex - num = re.findall(r'\d+', filename) - if num: # If there are digits in the filename - max_num = max(max_num, int(num[-1])) # Use the last set of digits as the number - return max_num - - - def request_strikeforce(username, auth_token, base, request, parameters): ''' Takes in auth values and api call parameters and returns the data about @@ -87,7 +77,7 @@ def request_strikeforce(username, auth_token, base, request, parameters): try: info = json.loads(response.text) return info - except JSONDecodeError: + except json.decoder.JSONDecodeError: logging.warning('An error occurred while decoding JSON') info = 0 return info @@ -95,7 +85,7 @@ def request_strikeforce(username, auth_token, base, request, parameters): logging.warning("Connection Error, max retries exceeded") info = 0 return info - + def fetch_image_api(config): ''' @@ -113,12 +103,13 @@ def fetch_image_api(config): accounts = config['username_scraper'] tokens = config['auth_token'] path = "./last_id.txt" - password = config['password_scraper'] + visualize_output = config['visualize_output'] + unlabeled_img = config['path_to_unlabeled_output'] checkfile = os.path.exists(path) if checkfile is False: new_file = open("last_id.txt", "x") new_file.close() - first_id = str(0) # function to get the most recent id from sf) + first_id = str(0) # function to get the most recent id from sf) new_file = open('last_id.txt', 'w') new_file.writelines(first_id) new_file.close() @@ -136,7 +127,7 @@ def fetch_image_api(config): "photos/recent", "limit=12") if data == 0: new_photos = [] - logging.warning('Returning to main loop after failed http request, will try again') + logging.warning('Returning to main loop after failed http request') return new_photos else: photos += data['photos']['data'] @@ -149,22 +140,24 @@ def fetch_image_api(config): logging.info(info) try: camera = camera_names[photos[i]['relationships'] - ['camera']['data']['id']] + ['camera']['data']['id']] except KeyError: - logging.warning('Cannot retrieve photo from camera as there is no associated ID in the config file') + logging.warning('skipped img: no associated cam ID') continue newname = config['save_dir'] + camera newname += "_" + info['file_thumb_filename'] urllib.request.urlretrieve(info['file_thumb_url'], newname) new_photos.append([photos[i]['id'], info['file_thumb_url'], newname]) - newname = '/home/katiedemo/unlabeled_photos/' + 'image' - new_file_num = get_last_file_number('/home/katiedemo/unlabeled_photos') - new_file_num = new_file_num + 1 - new_file_num = str(new_file_num) - newname += "_" + new_file_num - urllib.request.urlretrieve(info['file_thumb_url'], newname) + if visualize_output is True: + file_path = create_folder(unlabeled_img) + newname = file_path + 'image' + new_file_num = get_last_file_number(file_path) + new_file_num = new_file_num + 1 + new_file_num = str(new_file_num) + newname += "_" + new_file_num + urllib.request.urlretrieve(info['file_thumb_url'], newname) new_photos = np.array(new_photos) if len(new_photos) > 0: # update last image diff --git a/cougarvision_visualize/display.py b/cougarvision_visualize/display.py new file mode 100644 index 0000000..2546625 --- /dev/null +++ b/cougarvision_visualize/display.py @@ -0,0 +1,150 @@ +'''CougarVision Visualize Output +This script is intended to be run alongside fetch_and_alert.py in a second +terminal or tmux terminal. It displays the most recent classified detections +on a 3x3 grid on one screen and most recent images on a second 9x9 grid. + - cougarvision conda environment must be activated + - fetch_and_alert.py must be run with visualize_output: param set to 'True' + - path_to_unlabeled_output: and path_to_labeled_output: parameters filled + out in the config file. + *fetch_and_alert.py will create the folders for you + if you only include the paths but the folders are not yet created. +This script assumes 2 monitors and will display a blank screen on either +display if the minimum number of images is not met, 9 for screen 1 and 81 +for screen 2. Later versions will account for this and still display images, +but for now if that is an issue you can fill the folder with black images with +the correct nomenclature: image_1.jpg, image_2.jpg... and it will replace the +black images as they come in. +''' + + +import os +import time +import argparse +import numpy as np +import yaml +import cv2 +from screeninfo import get_monitors + + +def get_screen_resolutions(): + '''Function to get the screen resolutions for both monitors''' + monitors = get_monitors() + resolutions = [(monitor.width, monitor.height) for monitor in monitors] + return resolutions + + +def get_newest_images(folder_path, num_images): + '''Function to return the newest x num of images from folder''' + files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))] + + if not files: + return [] + + def sort_key_func(file_name): + try: + return int(os.path.splitext(file_name.split('_')[1])[0]) + except ValueError: + return float('-inf') + + files.sort(key=sort_key_func, reverse=True) + newest_files = files[:num_images] + images = [cv2.imread(os.path.join(folder_path, file)) for file in newest_files] + images = [img for img in images if img is not None] + return images + + +def display_images(images, window_name='CougarVision'): + '''Function to display labeled 9 recent images in 3x3 grid''' + resolutions = get_screen_resolutions() + screen_height = resolutions[0][1] + screen_width = resolutions[0][0] + + num_images_per_row = 3 + num_images_per_col = 3 + + max_width_per_image = screen_width // num_images_per_row + max_height_per_image = screen_height // num_images_per_col + + display_img = np.zeros((screen_height, screen_width, 3), np.uint8) + + for i, img in enumerate(images): + if img is not None: + x_offset = (i % num_images_per_row) * max_width_per_image + y_offset = (i // num_images_per_row) * max_height_per_image + + resized_image = cv2.resize(img, (max_width_per_image, max_height_per_image)) + + display_img[y_offset:y_offset + max_height_per_image, x_offset:x_offset + max_width_per_image] = resized_image + + cv2.imshow(window_name, display_img) + + +def display_more_images(images, window_2='Newest Image'): + '''Function to display the 81 unlabeled images on 2nd monitor 9x9''' + resolutions = get_screen_resolutions() + screen_height = resolutions[1][1] + screen_width = resolutions[1][0] + + num_images_per_row = 9 + num_images_per_col = 9 + + max_width_per_image = screen_width // num_images_per_row + max_height_per_image = screen_height // num_images_per_col + + display_img = np.zeros((screen_height, screen_width, 3), np.uint8) + + for i, img in enumerate(images): + if img is not None: + x_offset = (i % num_images_per_row) * max_width_per_image + y_offset = (i // num_images_per_row) * max_height_per_image + + resized_image = cv2.resize(img, (max_width_per_image, max_height_per_image)) + + display_img[y_offset:y_offset + max_height_per_image, x_offset:x_offset + max_width_per_image] = resized_image + + cv2.imshow(window_2, display_img) + + +if __name__ == "__main__": + WINDOW_NAME = 'CougarVision' + WINDOW_2 = "Newest Image" + + RESOLUTIONS = get_screen_resolutions() + + cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_NORMAL) + cv2.namedWindow(WINDOW_2, cv2.WINDOW_NORMAL) + + cv2.moveWindow(WINDOW_NAME, 0, 0) + cv2.moveWindow(WINDOW_2, RESOLUTIONS[0][0], 0) + + cv2.setWindowProperty(WINDOW_NAME, cv2.WND_PROP_FULLSCREEN, + cv2.WINDOW_FULLSCREEN) + cv2.setWindowProperty(WINDOW_2, cv2.WND_PROP_FULLSCREEN, + cv2.WINDOW_FULLSCREEN) + + PARSER = argparse.ArgumentParser(description='Retrieves images from \ + email & web scraper & runs detection') + PARSER.add_argument('config', type=str, help='Path to config file') + ARGS = PARSER.parse_args() + CONFIG_FILE = ARGS.config + + with open(CONFIG_FILE, 'r', encoding='utf-8') as stream: + CONFIG = yaml.safe_load(stream) + LABELED = CONFIG['path_to_labeled_output'] + UNLABELED = CONFIG['path_to_unlabeled_output'] + + while True: + NEW_IMG = get_newest_images(LABELED, 9) + if len(NEW_IMG) >= 9: + display_images(NEW_IMG, WINDOW_NAME) + + NEWER_IMG = get_newest_images(UNLABELED, 81) + if len(NEWER_IMG) >= 81: + display_more_images(NEWER_IMG, WINDOW_2) + + time.sleep(1) + + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + cv2.destroyAllWindows() diff --git a/cougarvision_visualize/visualize_helper.py b/cougarvision_visualize/visualize_helper.py new file mode 100644 index 0000000..5b8c8d8 --- /dev/null +++ b/cougarvision_visualize/visualize_helper.py @@ -0,0 +1,25 @@ +import os +import re + + +def get_last_file_number(folder_path): + max_num = 0 + for filename in os.listdir(folder_path): + # Extract digits from the filename using regex + num = re.findall(r'\d+', filename) + if num: # If there are digits in the filename + max_num = max(max_num, int(num[-1])) # Use the last set of digits as the number + return max_num + + +def create_folder(folder_path): + """ + Check if a folder exists at the specified path, and create it if it doesn't. + + folder_path: The path of the folder to check and potentially create. + return: path + """ + if not os.path.exists(folder_path): + os.makedirs(folder_path) + return folder_path + diff --git a/demo2.py b/demo2.py deleted file mode 100644 index 42b24e4..0000000 --- a/demo2.py +++ /dev/null @@ -1,117 +0,0 @@ -import cv2 -import os -import time -from screeninfo import get_monitors -import numpy as np - - -def get_screen_resolutions(): - monitors = get_monitors() - resolutions = [(monitor.width, monitor.height) for monitor in monitors] - return resolutions - - -def get_newest_images(folder_path, num_images): - files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))] - - if not files: - return [] - - def sort_key_func(file_name): - try: - return int(os.path.splitext(file_name.split('_')[1])[0]) - except ValueError: - return float('-inf') - - files.sort(key=sort_key_func, reverse=True) - newest_files = files[:num_images] - images = [cv2.imread(os.path.join(folder_path, file)) for file in newest_files] - images = [img for img in images if img is not None] - return images - - -def display_images(images, window_name='CougarVision'): - resolutions = get_screen_resolutions() - screen_height = resolutions[0][1] - screen_width = resolutions[0][0] - - num_images_per_row = 3 - num_images_per_col = 3 - - max_width_per_image = screen_width // num_images_per_row - max_height_per_image = screen_height // num_images_per_col - - display_img = np.zeros((screen_height, screen_width, 3), np.uint8) - - for i, img in enumerate(images): - if img is not None: - x_offset = (i % num_images_per_row) * max_width_per_image - y_offset = (i // num_images_per_row) * max_height_per_image - - resized_image = cv2.resize(img, (max_width_per_image, max_height_per_image)) - - display_img[y_offset:y_offset + max_height_per_image, x_offset:x_offset + max_width_per_image] = resized_image - - cv2.imshow(window_name, display_img) - -def display_more_images(images, window_name='Newest Image'): - resolutions = get_screen_resolutions() - screen_height = resolutions[1][1] - screen_width = resolutions[1][0] - - num_images_per_row = 9 - num_images_per_col = 9 - - max_width_per_image = screen_width // num_images_per_row - max_height_per_image = screen_height // num_images_per_col - - display_img = np.zeros((screen_height, screen_width, 3), np.uint8) - - for i, img in enumerate(images): - if img is not None: - x_offset = (i % num_images_per_row) * max_width_per_image - y_offset = (i // num_images_per_row) * max_height_per_image - - resized_image = cv2.resize(img, (max_width_per_image, max_height_per_image)) - - display_img[y_offset:y_offset + max_height_per_image, x_offset:x_offset + max_width_per_image] = resized_image - - cv2.imshow(window_name, display_img) - - -if __name__ == "__main__": - window_name = 'CougarVision' - newest_window_name = "Newest Image" - - resolutions = get_screen_resolutions() - - cv2.namedWindow(window_name, cv2.WINDOW_NORMAL) - cv2.namedWindow(newest_window_name, cv2.WINDOW_NORMAL) - - cv2.moveWindow(window_name, 0, 0) - cv2.moveWindow(newest_window_name, resolutions[0][0], 0) - - cv2.setWindowProperty(window_name, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN) - cv2.setWindowProperty(newest_window_name, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN) - - while True: - folder_path_for_9 = '/home/katiedemo/demo_images' - num_images = 9 - newest_images = get_newest_images(folder_path_for_9, num_images) - if len(newest_images) >= 9: - display_images(newest_images, window_name) - - folder_path_for_81 = '/home/katiedemo/unlabeled_photos' - number_images = 81 - newester_images = get_newest_images(folder_path_for_81, number_images) - if len(newester_images) >= 81: - display_more_images(newester_images, newest_window_name) - - time.sleep(1) - - if cv2.waitKey(1) & 0xFF == ord('q'): - break - - cv2.destroyAllWindows() - - diff --git a/fetch_and_alert.py b/fetch_and_alert.py index ac44110..78da734 100644 --- a/fetch_and_alert.py +++ b/fetch_and_alert.py @@ -22,16 +22,16 @@ import time import warnings from datetime import datetime as dt +import logging import yaml import schedule -import logging from cougarvision_utils.detect_img import detect from cougarvision_utils.alert import checkin from cougarvision_utils.get_images import fetch_image_api -#from sageranger.post_monthly import post_monthly_obs -from animl.predictSpecies import load_classifier -from animl.detectMD import load_MD_model +# from sageranger.post_monthly import post_monthly_obs +from animl.classify import load_classifier +from animl import megadetector # Numpy FutureWarnings from tensorflow import @@ -55,6 +55,7 @@ DEV_EMAILS = CONFIG['dev_emails'] HOST = 'imap.gmail.com' RUN_SCHEDULER = CONFIG['run_scheduler'] +VISUALIZE_OUTPUT = CONFIG['visualize_output'] # Set interval for checking in @@ -62,10 +63,11 @@ # load models once CLASSIFIER_MODEL = load_classifier(CLASSIFIER) -DETECTOR_MODEL = load_MD_model(DETECTOR) +DETECTOR_MODEL = megadetector.MegaDetector(DETECTOR) def logger(): + '''Function to define logging file parameters''' logging.basicConfig(filename='cougarvision.log', format='%(levelname)s:%(asctime)s:%(module)s:%(funcName)s: %(message)s', level=logging.DEBUG) @@ -87,10 +89,13 @@ def main(): ''''Runs main program and schedules future runs''' logger() fetch_detect_alert() - schedule.every(RUN_SCHEDULER).seconds.do(fetch_detect_alert) + if VISUALIZE_OUTPUT is True: + schedule.every(RUN_SCHEDULER).seconds.do(fetch_detect_alert) + else: + schedule.every(RUN_SCHEDULER).minutes.do(fetch_detect_alert) schedule.every(CHECKIN_INTERVAL).hours.do(checkin, DEV_EMAILS, USERNAME, PASSWORD, HOST) - #schedule.every(30).days.do(post_monthly_obs, TOKEN, AUTH) + # schedule.every(30).days.do(post_monthly_obs, TOKEN, AUTH) while True: schedule.run_pending() From 4c4243cdb605234607d25b71181a3639abb94c00 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Tue, 28 Nov 2023 10:42:27 -0800 Subject: [PATCH 03/12] Change filename for cougarvision_visualize script Was called visualize_output now called visualize_helper --- cougarvision_utils/detect_img.py | 4 ++-- cougarvision_utils/get_images.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cougarvision_utils/detect_img.py b/cougarvision_utils/detect_img.py index 25a62b7..e266266 100644 --- a/cougarvision_utils/detect_img.py +++ b/cougarvision_utils/detect_img.py @@ -23,8 +23,8 @@ from cougarvision_utils.cropping import draw_bounding_box_on_image from cougarvision_utils.alert import smtp_setup, send_alert -from cougarvision_visualize.visualize_output import get_last_file_number -from cougarvision_visualize.visualize_output import create_folder +from cougarvision_visualize.visualize_helper import get_last_file_number +from cougarvision_visualize.visualize_helper import create_folder with open("config/cameratraps.yml", 'r') as stream: diff --git a/cougarvision_utils/get_images.py b/cougarvision_utils/get_images.py index d772097..73d5eaa 100644 --- a/cougarvision_utils/get_images.py +++ b/cougarvision_utils/get_images.py @@ -18,8 +18,8 @@ import logging import requests import numpy as np -from cougarvision_visualize.visualize_output import get_last_file_number -from cougarvision_visualize.visualize_output import create_folder +from cougarvision_visualize.visualize_helper import get_last_file_number +from cougarvision_visualize.visualize_helper import create_folder ''' From bfddaa7646056a92208beef59ad106a2c700ab6e Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Tue, 28 Nov 2023 11:07:03 -0800 Subject: [PATCH 04/12] Condence function import and improve specificity on logging msg --- cougarvision_utils/get_images.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cougarvision_utils/get_images.py b/cougarvision_utils/get_images.py index 73d5eaa..2e51e82 100644 --- a/cougarvision_utils/get_images.py +++ b/cougarvision_utils/get_images.py @@ -18,8 +18,7 @@ import logging import requests import numpy as np -from cougarvision_visualize.visualize_helper import get_last_file_number -from cougarvision_visualize.visualize_helper import create_folder +from cougarvision_visualize.visualize_helper import get_last_file_number, create_folder ''' @@ -71,7 +70,7 @@ def request_strikeforce(username, auth_token, base, request, parameters): ''' call = base + request + "?" + parameters try: - logging.info("Getting new image data from Strikeforce") + logging.info("Getting new Strikeforce image data from: " + username) response = requests.get(call, headers={"X-User-Email": username, "X-User-Token": auth_token}) try: From 5a2f8a5e0f2974fd86cd985d5bef1a1d6ea64095 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Tue, 6 Feb 2024 11:47:04 -0800 Subject: [PATCH 05/12] Fix logging duplicates The script was not writing the log to a file, only to the terminal and was logging duplicated lines. It needed to be initialized before calling the classifier and detector initializers because when those got called first, they introduced their own logging handlers that overrode the one I created. Also the force=True needed to be added so that the log file would actually be created --- fetch_and_alert.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/fetch_and_alert.py b/fetch_and_alert.py index 78da734..d7aa134 100644 --- a/fetch_and_alert.py +++ b/fetch_and_alert.py @@ -26,6 +26,7 @@ import yaml import schedule + from cougarvision_utils.detect_img import detect from cougarvision_utils.alert import checkin from cougarvision_utils.get_images import fetch_image_api @@ -58,17 +59,30 @@ VISUALIZE_OUTPUT = CONFIG['visualize_output'] +def logger(): + '''Function to define logging file parameters''' + msg_intro = "%(levelname)s:%(asctime)s:%(module)s:%(funcName)s:" + msg_intro = msg_intro + " %(message)s" + logging.basicConfig(filename='cougarvision.log', + format=msg_intro, + level=logging.INFO, + force=True) + + +# Initialize logger now because it will protect against +# handlers that get created when classifer and detector are intialized +logger() + + # Set interval for checking in CHECKIN_INTERVAL = CONFIG['checkin_interval'] - +print("Loading classifier") # load models once CLASSIFIER_MODEL = load_classifier(CLASSIFIER) +print("Finished loading classifier") +print("Begin loading detector") DETECTOR_MODEL = megadetector.MegaDetector(DETECTOR) - - -def logger(): - '''Function to define logging file parameters''' - logging.basicConfig(filename='cougarvision.log', format='%(levelname)s:%(asctime)s:%(module)s:%(funcName)s: %(message)s', level=logging.DEBUG) +print("Finished loading detector") def fetch_detect_alert(): @@ -87,7 +101,6 @@ def fetch_detect_alert(): def main(): ''''Runs main program and schedules future runs''' - logger() fetch_detect_alert() if VISUALIZE_OUTPUT is True: schedule.every(RUN_SCHEDULER).seconds.do(fetch_detect_alert) From 9e7b8f968a7af6ed3ac0e4c5d8eb64b0de78b9d4 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Fri, 16 Feb 2024 11:51:35 -0800 Subject: [PATCH 06/12] Fix pylint and flake8 errors There's a few "too many variables" and "unable to import" errors but I'm ignoring those --- cougarvision_utils/detect_img.py | 3 +- cougarvision_utils/get_images.py | 29 ++++++++++-------- cougarvision_visualize/display.py | 51 ++++++++++++++++++------------- 3 files changed, 46 insertions(+), 37 deletions(-) diff --git a/cougarvision_utils/detect_img.py b/cougarvision_utils/detect_img.py index e266266..28d258e 100644 --- a/cougarvision_utils/detect_img.py +++ b/cougarvision_utils/detect_img.py @@ -98,8 +98,7 @@ def detect(images, config, c_model, d_model): max_df = parse_results.from_classifier(animal_df, predictions, classes, - None, - False) + None) # Creates a data frame with all relevant data cougars = max_df[max_df['prediction'].isin(targets)] # drops all detections with confidence less than threshold diff --git a/cougarvision_utils/get_images.py b/cougarvision_utils/get_images.py index 2e51e82..5fe58d3 100644 --- a/cougarvision_utils/get_images.py +++ b/cougarvision_utils/get_images.py @@ -18,35 +18,36 @@ import logging import requests import numpy as np -from cougarvision_visualize.visualize_helper import get_last_file_number, create_folder +from cougarvision_visualize.visualize_helper import get_last_file_number +from cougarvision_visualize.visualize_helper import create_folder ''' -#request examples +request examples -#get list of camaras +get list of camaras request <- "cameras" parameters <- "" -#recent photo count +recent photo count request <- "photos/recent/count" parameters <- "" -#get recent photos across cameras +get recent photos across cameras request <- "photos/recent" parameters <- "limit=100" -#get photos from specific camera (will need to loop through pages) +get photos from specific camera (will need to loop through pages) request <- "photos" parameters <- "page=3&sort_date=desc&camera_id[]=59681" -#get photos from specific camera filtered by date (will need -# to loop through pages) +get photos from specific camera filtered by date (will need +to loop through pages) request <- "photos" parameters <- "page=1&sort_date=desc&camera_id[]= -#60272&date_start=2022-09-01&date_end=2022-10-07" +60272&date_start=2022-09-01&date_end=2022-10-07" -#get subscriptions +get subscriptions request <- "subscriptions" parameters <- "" ''' @@ -70,7 +71,7 @@ def request_strikeforce(username, auth_token, base, request, parameters): ''' call = base + request + "?" + parameters try: - logging.info("Getting new Strikeforce image data from: " + username) + logging.debug("Getting new Strikeforce image data from: " + username) response = requests.get(call, headers={"X-User-Email": username, "X-User-Token": auth_token}) try: @@ -127,9 +128,11 @@ def fetch_image_api(config): if data == 0: new_photos = [] logging.warning('Returning to main loop after failed http request') + error_message = "Warning: Failed http request, will retry " + error_message = error_message + "from main loop, check connection" + print(error_message) return new_photos - else: - photos += data['photos']['data'] + photos += data['photos']['data'] new_photos = [] photos = sorted(photos, key=lambda x: x['attributes']['original_datetime']) diff --git a/cougarvision_visualize/display.py b/cougarvision_visualize/display.py index 2546625..19f8703 100644 --- a/cougarvision_visualize/display.py +++ b/cougarvision_visualize/display.py @@ -6,6 +6,8 @@ - fetch_and_alert.py must be run with visualize_output: param set to 'True' - path_to_unlabeled_output: and path_to_labeled_output: parameters filled out in the config file. + - the command line argument to run is python3 display.py + *fetch_and_alert.py will create the folders for you if you only include the paths but the folders are not yet created. This script assumes 2 monitors and will display a blank screen on either @@ -14,6 +16,7 @@ but for now if that is an issue you can fill the folder with black images with the correct nomenclature: image_1.jpg, image_2.jpg... and it will replace the black images as they come in. + ''' @@ -33,11 +36,11 @@ def get_screen_resolutions(): return resolutions -def get_newest_images(folder_path, num_images): +def get_newest_images(f_p, num_images): '''Function to return the newest x num of images from folder''' - files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))] + fil = [f for f in os.listdir(f_p) if os.path.isfile(os.path.join(f_p, f))] - if not files: + if not fil: return [] def sort_key_func(file_name): @@ -46,9 +49,9 @@ def sort_key_func(file_name): except ValueError: return float('-inf') - files.sort(key=sort_key_func, reverse=True) - newest_files = files[:num_images] - images = [cv2.imread(os.path.join(folder_path, file)) for file in newest_files] + fil.sort(key=sort_key_func, reverse=True) + newest_files = fil[:num_images] + images = [cv2.imread(os.path.join(f_p, file)) for file in newest_files] images = [img for img in images if img is not None] return images @@ -59,22 +62,24 @@ def display_images(images, window_name='CougarVision'): screen_height = resolutions[0][1] screen_width = resolutions[0][0] - num_images_per_row = 3 - num_images_per_col = 3 + num_images_row = 3 + num_images_col = 3 - max_width_per_image = screen_width // num_images_per_row - max_height_per_image = screen_height // num_images_per_col + max_w_image = screen_width // num_images_row + max_h_image = screen_height // num_images_col display_img = np.zeros((screen_height, screen_width, 3), np.uint8) for i, img in enumerate(images): if img is not None: - x_offset = (i % num_images_per_row) * max_width_per_image - y_offset = (i // num_images_per_row) * max_height_per_image + x_offset = (i % num_images_row) * max_w_image + y_offset = (i // num_images_row) * max_h_image - resized_image = cv2.resize(img, (max_width_per_image, max_height_per_image)) + resized_image = cv2.resize(img, (max_w_image, max_h_image)) - display_img[y_offset:y_offset + max_height_per_image, x_offset:x_offset + max_width_per_image] = resized_image + y_slice = slice(y_offset, y_offset + max_h_image) + x_slice = slice(x_offset, x_offset + max_w_image) + display_img[y_slice, x_slice] = resized_image cv2.imshow(window_name, display_img) @@ -85,22 +90,24 @@ def display_more_images(images, window_2='Newest Image'): screen_height = resolutions[1][1] screen_width = resolutions[1][0] - num_images_per_row = 9 - num_images_per_col = 9 + num_images_row = 9 + num_images_col = 9 - max_width_per_image = screen_width // num_images_per_row - max_height_per_image = screen_height // num_images_per_col + max_w_image = screen_width // num_images_row + max_h_image = screen_height // num_images_col display_img = np.zeros((screen_height, screen_width, 3), np.uint8) for i, img in enumerate(images): if img is not None: - x_offset = (i % num_images_per_row) * max_width_per_image - y_offset = (i // num_images_per_row) * max_height_per_image + x_offset = (i % num_images_row) * max_w_image + y_offset = (i // num_images_row) * max_h_image - resized_image = cv2.resize(img, (max_width_per_image, max_height_per_image)) + resized_image = cv2.resize(img, (max_w_image, max_h_image)) - display_img[y_offset:y_offset + max_height_per_image, x_offset:x_offset + max_width_per_image] = resized_image + y_slice = slice(y_offset, y_offset + max_h_image) + x_slice = slice(x_offset, x_offset + max_w_image) + display_img[y_slice, x_slice] = resized_image cv2.imshow(window_2, display_img) From 68e66d03c90de637cfc4eb4bf28ce9aa4fbe86c4 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Mon, 4 Mar 2024 23:16:50 +0000 Subject: [PATCH 07/12] Handle .pt classifier models We trained a new model 'southwest_v3.pt' that performs better on cougars than the previous model, but the animl-py on pypi can only handle h5 models. Cougarvision had to be updated to be compatible with the updates that have been made to animl-py on github where there exists the capability to use .pt models. It was mostly function name changes and movement, and the updates only affected fetch_and_alert.py for loading the classifier model and detect_img.py. --- cougarvision_utils/detect_img.py | 13 ++++++------- fetch_and_alert.py | 5 +++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cougarvision_utils/detect_img.py b/cougarvision_utils/detect_img.py index 28d258e..b0eecce 100644 --- a/cougarvision_utils/detect_img.py +++ b/cougarvision_utils/detect_img.py @@ -17,9 +17,9 @@ import logging import yaml from PIL import Image -from animl import parse_results, classify, split +from animl import inference, split from sageranger import is_target, attach_image, post_event -from animl.detectMD import detect_MD_batch +from animl.detect import detect_MD_batch, parse_MD from cougarvision_utils.cropping import draw_bounding_box_on_image from cougarvision_utils.alert import smtp_setup, send_alert @@ -73,23 +73,22 @@ def detect(images, config, c_model, d_model): checkpoint_path=None, confidence_threshold=confidence, checkpoint_frequency=checkpoint_f, - results=None, quiet=False, image_size=None) end = time.time() md_time = end - start logging.debug('Time to detect: ' + str(md_time)) # Parse results - data_frame = parse_results.from_MD(results, None, None) + data_frame = parse_MD(results, None, None) # filter out all non animal detections if not data_frame.empty: - animal_df = split.getAnimals(data_frame) - otherdf = split.getEmpty(data_frame) + animal_df = split.get_animals(data_frame) + otherdf = split.get_empty(data_frame) # run classifier on animal detections if there are any if not animal_df.empty: # create generator for images start = time.time() - predictions = classify.predict_species(animal_df, c_model, + predictions = inference.predict_species(animal_df, c_model, batch=4) end = time.time() cls_time = end - start diff --git a/fetch_and_alert.py b/fetch_and_alert.py index d7aa134..f02af06 100644 --- a/fetch_and_alert.py +++ b/fetch_and_alert.py @@ -31,7 +31,7 @@ from cougarvision_utils.alert import checkin from cougarvision_utils.get_images import fetch_image_api # from sageranger.post_monthly import post_monthly_obs -from animl.classify import load_classifier +from animl.classifiers import load_model from animl import megadetector @@ -57,6 +57,7 @@ HOST = 'imap.gmail.com' RUN_SCHEDULER = CONFIG['run_scheduler'] VISUALIZE_OUTPUT = CONFIG['visualize_output'] +LABELS = CONFIG['classes'] def logger(): @@ -78,7 +79,7 @@ def logger(): CHECKIN_INTERVAL = CONFIG['checkin_interval'] print("Loading classifier") # load models once -CLASSIFIER_MODEL = load_classifier(CLASSIFIER) +CLASSIFIER_MODEL, CLASSES = load_model(CLASSIFIER, LABELS) print("Finished loading classifier") print("Begin loading detector") DETECTOR_MODEL = megadetector.MegaDetector(DETECTOR) From e5ba67c63cd9da3a4795ef55c0bb6c88c9229df6 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Mon, 4 Mar 2024 15:45:09 -0800 Subject: [PATCH 08/12] Add some new config values/change names The example config file was missing some of the parameters needed by cougarvision wild_demo update. This config file now exposes the color for the bounding box, sending email alerts, the time between fetch_and_alert function call, and changed the names of the config values for dev and consumer emails. --- config/fetch_and_alert.yml | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/config/fetch_and_alert.yml b/config/fetch_and_alert.yml index 31ace9c..6909c69 100644 --- a/config/fetch_and_alert.yml +++ b/config/fetch_and_alert.yml @@ -1,7 +1,9 @@ #home_dir - path to local cougarvision repo home_dir: /home/johnsmith/cougarvision -# 1 = email alerts and 2 = earthranger alerts -use_variation: 2 +#True to send detections to consumer emails +email_alerts: True +#True to send detections to earthranger +er_alerts: True #detector_model - path to detector model detector_model: /home/johnsmith/cougarvision/detector_models/megadetector_v4.1.pb #classifier_model - path to classifier model @@ -11,17 +13,20 @@ checkpoint_frequency: -1 log_dir: /home/johnsmith/cougarvision/logs/ #classes - path to the class list for the classifier model classes: /home/johnsmith/cougarvision/labels/sw_classes.txt -#to_email - the email that will receive email alerts, can be multiple emails #Run version that allows for images cast to two screens? True/False visualize_output: True +#for visualizing output, folder for cougarvision to put all images path_to_unlabeled_output: /path/to/unlabeled/img/folder +#for visualizing output, folder for cougarvision detections with bounding boxes path_to_labeled_output: /path/to/labeled/img/folder -to_emails: [your@email.com] +#the emails that will receive email alerts, can be multiple emails +consumer_emails: [your@email.com] +#emails to recieve daily check-in and confidence of detections +dev_emails: [your@email.com] #threshold confidence score confidence: 0.7 #cpu threads threads: 8 -# Web Scraping #strike force api url strikeforce_api: https://api.strikeforcewireless.com/api/v2/ #strikeforce wireless username @@ -34,18 +39,20 @@ auth_token: save_dir: /home/johnsmith/images/ #dict of camera names and their respective strikeforce wireless ids, these must be retrieved from #strikeforce wireless web dev tools in the code and currently the cam names must be 4 chars long -camera_names: {'62201': B018, '59760': B016, '59681': B013, '59758': B015, '60095': B014, - '60272': B017, '85767': B041, '105353': B044, '105355': B045, '105357': B046, '105359': B047, - '105360': B048, '104322': B049} +camera_names: {'62201': B018, '59760': B016...} #animals that the detector will send alerts for if detected (from class list) alert_targets: [cougar, bobcat, skunk, deer, dog, coyote, rabbit, elephant] #checkn_interval - time interval between still-alive emails to let us know no errors have crashed #cougarvision if it's running constantly checkin_interval: 24 -#email to send still-alive email to -admin: admin@email.com #x-csrf token retreived from earthranger interactive api example requests, must be logged into an #existing earthranger account with interactive api access token: [insert token] #authorization bearer token retreived from same earthranger interactive api example requests authorization: Bearer [insert authorization] +#the time between repeating running fetch_and_alert, if visualize_output = True, it will be in seconds and if it = False it will be in minutes +run_scheduler: 10 +#color of the bounding box +color: 'LawnGreen' +#color: 'Orange' +#color: 'Azure' From 749b716562ac7ab6740be1521404f24b9ea939e6 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Tue, 26 Mar 2024 16:37:02 +0000 Subject: [PATCH 09/12] Remove dependency of old animl function There was an old animl function called parse_results that prepared the classification output for alert sending this has been depricated and that old function is no longer necessary --- cougarvision_utils/detect_img.py | 23 +++++++++-------------- fetch_and_alert.py | 2 +- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/cougarvision_utils/detect_img.py b/cougarvision_utils/detect_img.py index b0eecce..c64a384 100644 --- a/cougarvision_utils/detect_img.py +++ b/cougarvision_utils/detect_img.py @@ -32,7 +32,7 @@ sys.path.append(camera_traps_config['camera_traps_path']) -def detect(images, config, c_model, d_model): +def detect(images, config, c_model, classes, d_model): ''' This function takes in a dataframe of images and runs a detector model, classifies the species of interest, and sends alerts either to email or an @@ -49,9 +49,9 @@ def detect(images, config, c_model, d_model): email_alerts = bool(config['email_alerts']) er_alerts = bool(config['er_alerts']) log_dir = config['log_dir'] + class_list = config['classes'] checkpoint_f = config['checkpoint_frequency'] confidence = config['confidence'] - classes = config['classes'] targets = config['alert_targets'] username = config['username'] password = config['password'] @@ -88,20 +88,16 @@ def detect(images, config, c_model, d_model): if not animal_df.empty: # create generator for images start = time.time() - predictions = inference.predict_species(animal_df, c_model, - batch=4) + predictions = inference.predict_species(animal_df.reset_index(drop=True), c_model, classes, file_col="file") end = time.time() cls_time = end - start + print("Time to classify: ") + print(cls_time) logging.debug('Time to classify: ' + str(cls_time)) - # Parse results - max_df = parse_results.from_classifier(animal_df, - predictions, - classes, - None) - # Creates a data frame with all relevant data - cougars = max_df[max_df['prediction'].isin(targets)] + # checks to see if predicted class is in targets + cougars = predictions[predictions['prediction'].isin(targets)] # drops all detections with confidence less than threshold - cougars = cougars[cougars['conf'] >= confidence] + cougars = cougars[cougars['confidence'] >= confidence] # reset dataframe index cougars = cougars.reset_index(drop=True) # create a row in the dataframe containing only the camera name @@ -111,8 +107,7 @@ def detect(images, config, c_model, d_model): for idx in range(len(cougars.index)): label = cougars.at[idx, 'prediction'] # uncomment this line to use conf value for dev email alert - prob = str(cougars.at[idx, 'conf']) - label = cougars.at[idx, 'class'] + prob = str(cougars.at[idx, 'confidence']) img = Image.open(cougars.at[idx, 'file']) draw_bounding_box_on_image(img, cougars.at[idx, 'bbox2'], diff --git a/fetch_and_alert.py b/fetch_and_alert.py index f02af06..fa5e608 100644 --- a/fetch_and_alert.py +++ b/fetch_and_alert.py @@ -95,7 +95,7 @@ def fetch_detect_alert(): print('Finished fetching images') print('Starting Detection') for i in images: - detect(i, CONFIG, CLASSIFIER_MODEL, DETECTOR_MODEL) + detect(i, CONFIG, CLASSIFIER_MODEL, CLASSES, DETECTOR_MODEL) print('Finished Detection') print("Sleeping since: " + str(dt.now())) From ae22184516ccd4f47ab1cebecd2121ab7da830c5 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Tue, 26 Mar 2024 16:42:40 +0000 Subject: [PATCH 10/12] updated yml outline the example config needed to be updated to reflect changes --- config/fetch_and_alert.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config/fetch_and_alert.yml b/config/fetch_and_alert.yml index 31ace9c..6efcdb1 100644 --- a/config/fetch_and_alert.yml +++ b/config/fetch_and_alert.yml @@ -1,7 +1,8 @@ #home_dir - path to local cougarvision repo home_dir: /home/johnsmith/cougarvision # 1 = email alerts and 2 = earthranger alerts -use_variation: 2 +email_alerts: True +er_alerts: True #detector_model - path to detector model detector_model: /home/johnsmith/cougarvision/detector_models/megadetector_v4.1.pb #classifier_model - path to classifier model @@ -16,7 +17,8 @@ classes: /home/johnsmith/cougarvision/labels/sw_classes.txt visualize_output: True path_to_unlabeled_output: /path/to/unlabeled/img/folder path_to_labeled_output: /path/to/labeled/img/folder -to_emails: [your@email.com] +consumer_emails: [your@email.com] +dev_emails: [dev@email.com] #threshold confidence score confidence: 0.7 #cpu threads From cbfd01f246f1586df944fc228d93d9e64ba492af Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Tue, 26 Mar 2024 17:14:43 +0000 Subject: [PATCH 11/12] Make readme.md reflect the updated master README.md --- README.md | 77 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index ab1e806..1b75c75 100644 --- a/README.md +++ b/README.md @@ -10,27 +10,21 @@ program. Its name is an homage both to the species for which it was originally developed and the mascot of Cal State San Marcos where Jared was then studying, which is also the cougar. -## Streaming -Stream_detect.py takes in a stream, runs a pipeline to detect various animals. An image is sent to specified emails/phone numbers based on config/stream_detect.yml. -If a lizard or a cougar is detected then a video is recorded for as long as that animal remains to be detected. +# Setting up Conda Environment +First, you must install conda (whatever the latest version of Anaconda is) on your machine, install instructions can be found here: -## Fetch and alert -Fetch_and_alert.py combines two functions to retrieve images from [Strikeforce](https://www.strikeforcewireless.com) site and email addresses. A webscrapper is used to retrieve images from Strikeforce username and password can be changed in the config/fetch_and_alert.yml file. Emails are accessed and images attached are extracted, email addresses may also be changed in the same config file. Once extracted these images -are ran through both detector and classification models. And alerts (sends emails/texts) if a cougar is detected. -To run fetch_and_alert.py, the config/fetch_and_alert.yml must be configured according to the notes in the file. The command line script to run is: +[Instructions to install conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html) +If you haven't done so already: ``` -python3 fetch_and_alert.py config/fetch_and_alert.yml - +sudo apt install git ``` +Now you should be ready to clone the repository: +``` +git clone https://github.com/conservationtechlab/cougarvision.git -## Processing Batch Images -Run_batch_images.py can take in an input folder and apply the detector/classifier pipeline to render annotated images along with an output.json file in directory classifications. The output.json contains the final classifications on the cropped images and can be used to compare against a ground truth set. There is also an intermediate output.json which holds the crop detections and is used by the script to crop images, this one can be moved by configuration in the yml file. - -## Setting up Conda Environment - -[Instructions to install conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html) - +cd cougarvision +``` The file **cougarvision_env.yml** describes the python version and various dependencies with specific version numbers. To activate the enviroment ``` @@ -39,21 +33,60 @@ conda env create -f cougarvision_env.yml conda activate cougarvision_env conda env list - ``` The first line creates the enviroment from the specifications file which only needs to be done once. The second line activates the environment which may need to be done each time you restart the terminal. -The third line is to test the environment installation where it will list all dependencies for that environment +The third line is to test the environment installation where it will list all dependencies for that environment. -## Dependencies +The environment must be activated anytime you wish to run the scripts within this package. -These scripts rely on the [CameraTraps Repo](https://github.com/microsoft/CameraTraps). Download this repo and place it anywhere. Add that path to config/cameratraps.yml. The CameraTraps repo will then be added to the python sys environment variables upon loading an individual script. +# Models -## Models +Detection and inference models can be placed anywhere. In each script's yml file (under the config directory) is a field where the path variables for each model can be specified. Linked are the current updated models along with their class lists for [Peru](https://sandiegozoo.box.com/s/jfw7ih8xedzsn83to91pg6gvaq1nj5bl),[Peru class list](https://sandiegozoo.box.com/s/xng8erxrvw6nz98h8xjtk8avnopfz6ev), [Southwest](https://sandiegozoo.box.com/s/x63lnaxw8hag39mczeommqy9tw4t0ht9), [Southwest class list](https://sandiegozoo.box.com/s/hn8nput5pxjc3toao57gfn4h6zo1lyng) and [Kenya](https://sandiegozoo.box.com/s/cwn5wss9gjibvf57xop2zgfmlih512lt), [Kenya class list](https://sandiegozoo.box.com/s/f5athitml7bedix0ubnccyg8npvsr6ip). The detection model that is currently integrated with fetch_and_alert.py functionality is [MDv5 using pytorch](https://github.com/ecologize/CameraTraps/releases/download/v5.0/md_v5a.0.0.pt) -Detection and inference models can be place anywhere. In each script's yml file (under the config directory) is a field where the path variables for each model can be specified. +# Fetch and alert +Fetch_and_alert.py combines two functions to retrieve images from [Strikeforce](https://www.strikeforcewireless.com) site and email addresses. Strikeforce username and password can be changed in the config/fetch_and_alert.yml file. Emails are accessed and images attached are extracted, email addresses may also be changed in the same config file. Once extracted these images are ran through both detector and classification models. And alerts (sends emails/texts/EarthRanger event) if a cougar (or any target animal you define in config as long as they are an option on the class list for your classification model) is detected. +To run fetch_and_alert.py, the config/fetch_and_alert.yml must be configured according to the notes in the file. The command line script to run is: +``` +python3 fetch_and_alert.py config/fetch_and_alert.yml + +``` +## Installing animl +``` +pip3 install animl +``` +## Obtaining Strikeforce camera dictionary/auth_token +In order to retrieve photos from strikeforce, you need the api auth token for your account. We currently have a python script within /cougarvision_utils/strikeforceget.py that can obtain this auth_token given the strikeforce account username and password. +Once you have this token, place it in 'auth_token' within the config file. +To obtain the unique camera IDs in Strikeforce, run the script within /cougarvision_utils/strikeforcegetcameras.py with your email and the auth_token obtained from strikekforceget.py and it will output the unique strikeforce ID along with your camera names. +``` +python3 strikeforceget.py +python3 strikeforcegetcameras.py +``` +In this version of CougarVision, all camera names need to be 4 characters long, so if your camera names exceed this length, shorten them to 4 characters within the config directory. + +## EarthRanger integration +CougarVision can optionally send detections for a species of interest to their specific camera location in EarthRanger. +For this functionality, the er_alerts config must be set to "True". +You also need to include an authorization token from EarthRanger. This can be obtained from .pamdas.org/api/v1.0/docs/interactive/. If you run the first example (das_server) get>try it out>execute, you will see a 'Curl' example input. There will be an X-CSRF and Authorization token, copy and paste these values into 'token' and 'authorization' in the config file, respectively (including 'Bearer ' for the authorization). These tokens will expire relatively quickly. To make them last longer, head on over to .pamdas.org/admin under 'Django OAuth Toolkit'>access tokens and paste the authorization token into the search bar(not including 'Bearer '). Click on the option that matches, and change the expiration date for however far out you'd like it to expire. It's important to note that you should keep this token secret, especially if it will not expire for a while. +In order for this integration to work, the camera name in EarthRanger must be the same as the 4 digit name in the camera dictionary in the config file. For ease of adding your cameras in the same format we have, there is a script in our [EarthRanger integration API package](https://github.com/conservationtechlab/sageranger/blob/main/sageranger/post_camera_er.py) that will add the cameras correctly. Verify that the cameras are visible on your EarthRanger map instance before proceeding with this integration. + +## Email alerts +In order to send email alerts, CougarVision needs to know from what email to send them. For this, any email will do, but you need to include the email and password to the email. We just created a gmail specifically for this purpose, and include the email and the password under 'username' and 'password' in the config file. + +## Additional setup +Create two directories anywhere, one called 'images' and one called 'logs'. Place their file paths in the config under 'save_dir' and 'log_dir'. This will be where all photos from strikeforce are stored locally and where all the dataframes containing detection information for each photo will live. + +# Streaming +Stream_detect.py takes in a stream, runs a pipeline to detect various animals. An image is sent to specified emails/phone numbers based on config/stream_detect.yml. +If a lizard or a cougar is detected then a video is recorded for as long as that animal remains to be detected. +These scripts rely on the [CameraTraps Repo](https://github.com/microsoft/CameraTraps). Download this repo and place it anywhere. Add that path to config. The CameraTraps repo will then be added to the python sys environment variables upon loading an individual script. + +# Processing Batch Images +Run_batch_images.py can take in an input folder and apply the detector/classifier pipeline to render annotated images along with an output.json file in directory classifications. The output.json contains the final classifications on the cropped images and can be used to compare against a ground truth set. There is also an intermediate output.json which holds the crop detections and is used by the script to crop images, this one can be moved by configuration in the yml file. +These scripts rely on the [CameraTraps Repo](https://github.com/microsoft/CameraTraps). Download this repo and place it anywhere. Add that path to config. The CameraTraps repo will then be added to the python sys environment variables upon loading an individual script. From b0e2efdd1c7f0ebef94d9cbe0d6ca5cb408522d0 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Mon, 26 May 2025 16:51:36 -0700 Subject: [PATCH 12/12] remove our camera id and names from example config --- config/fetch_and_alert.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/fetch_and_alert.yml b/config/fetch_and_alert.yml index c52a05b..ae50894 100644 --- a/config/fetch_and_alert.yml +++ b/config/fetch_and_alert.yml @@ -41,7 +41,7 @@ auth_token: save_dir: /home/johnsmith/images/ #dict of camera names and their respective strikeforce wireless ids, these must be retrieved from #strikeforce wireless web dev tools in the code and currently the cam names must be 4 chars long -camera_names: {'62201': B018, '59760': B016...} +camera_names: {'': , '': ...} #animals that the detector will send alerts for if detected (from class list) alert_targets: [cougar, bobcat, skunk, deer, dog, coyote, rabbit, elephant] #checkn_interval - time interval between still-alive emails to let us know no errors have crashed