diff --git a/config/fetch_and_alert.yml b/config/fetch_and_alert.yml index 116e3a5..e4092ef 100644 --- a/config/fetch_and_alert.yml +++ b/config/fetch_and_alert.yml @@ -13,11 +13,11 @@ detector_model_type: "" #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 -#log_dir - path to logs (must create this folder first) +#log_dir - path to logs log_dir: /home/johnsmith/cougarvision/logs/ #classes - path to the class list for the classifier model classes: /home/johnsmith/cougarvision/labels/sw_classes.txt -#Run version that allows for images cast to two screens? True/False +#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 @@ -34,9 +34,13 @@ threads: 8 #strike force api url strikeforce_api: https://api.strikeforcewireless.com/api/v2/ #strikeforce wireless username -username_scraper: yourusername.cam@gmail.com +#formmatt matters here +username_scraper: ['yourusername.cam@gmail.com'] #strikeforce wireless password -password_scraper: yourpassword +password_scraper: ['yourpassword'] +# without brackets +username: youremail@gmail.com +password: yourpassword #authorization token from Strike Force auth_token: #save_dir - path to where the images get stored (must create folder) diff --git a/cougarvision_utils/alert.py b/cougarvision_utils/alert.py index 06c1668..fb562c6 100644 --- a/cougarvision_utils/alert.py +++ b/cougarvision_utils/alert.py @@ -8,26 +8,30 @@ ''' import mimetypes +import logging from email.message import EmailMessage from smtplib import SMTP_SSL, SMTP_SSL_PORT from datetime import datetime as dt def smtp_setup(username, password, host): - '''SMTP Setup + """SMTP Setup This function creates a simple mail transfer protocol by taking in a host email, a username and password for an email. Args: - username: username for email to send message from, string from config - password: password for email message will be sent from, string from config - host: IMAP protocol to download gmail messages, initialized in - detect_img.py - - Returns: SMTP_SSL object logged into the mailing account specified in - config yml - ''' + username (str): username for email to send message from + string from config + password (str): password for email message will be sent from, + string from config + host (str): IMAP protocol to download gmail messages, initialized in + detect_img.py + + Returns: + SMTP_SSL object logged into the mailing account specified in + config yml + """ # Init sending mail smtp_server = SMTP_SSL(host, port=SMTP_SSL_PORT) smtp_server.set_debuglevel(1) # Show SMTP server interactions @@ -35,29 +39,29 @@ def smtp_setup(username, password, host): return smtp_server -def send_alert(alert, img, smtp_server, from_email, to_emails, dev, conf): - '''Send Alert +def send_alert(config, alert, img, dev, prob): + """Send Alert This function takes in the animal label, the image of the animal of interest, the SMTP server created, and the to and from emails to send the alert containing that specific image along with the confidence value. Args: - alert: label of animal that the alert is being created for - conf: confidence value of the classifier that the animal it says it - is is the animal it is - img: the PIL.Image of the image that is to be sent, to be converted - to binary - smtp_server: where the email is to be sent from, SMTP_SSL object - from_email: the outgoing address for the alert - to_emails: the emails the alert will be sent to, defined in config yml file - ''' + alert (str): label of animal that the alert is being created for + conf (float): confidence value of the classifier that the animal it says it + is is the animal it is + img(bytes): the PIL.Image of the image that is to be sent, to be converted + to binary + config (dict): holds the values of username, password, host, + dev/consumeremails for email setup and info for recipients. + """ # Construct Email Content email_message = EmailMessage() - email_message.add_header('To', ', '.join(to_emails)) - email_message.add_header('From', from_email) - email_message.add_header('Subject', 'Alert!') - email_message.add_header('X-Priority', '1') # Urgency, 1 highest, 5 lowest + email_message['To'] = (', '.join(config.consumer_emails)) + email_message['from'] = config.username + email_message['Subject'] = 'Alert!' + email_message['X-Priority'] = '1' # Urgency, 1 highest, 5 lowest + message = "" if dev == 0: message = "Potential " + alert + " detected by CougarVision "\ + "system.\n\nPlease review attached image to verify"\ @@ -66,36 +70,48 @@ def send_alert(alert, img, smtp_server, from_email, to_emails, dev, conf): + "and artifacts have been known to trigger the system." elif dev != 0: message = "Potential " + alert + " detected with confidence value: "\ - + conf - - email_message.set_content(message) + + prob # Prepare Image format binary_data = img.getvalue() # Attach image to email filename = 'detection.jpg' + email_message.set_content(message) + maintype, _, subtype = (mimetypes.guess_type(filename)[0] or 'application/octet-stream').partition("/") + email_message.add_attachment(binary_data, maintype=maintype, subtype=subtype, filename=filename) # Server sends email message - server = smtp_server + server = smtp_setup(config.username, config.password, config.host) server.send_message(email_message) + logging.info("Email Alert sent.") + server.quit() -def checkin(to_emails, username, password, host): - '''Sends server status to specified email at specified time interval''' +def checkin(config): + """Sends server status to specified email at specified time interval + + Args: + config (dict): holds the values of username, password, host, + dev/consumeremails for email setup and info for recipients. + """ print("Checking in at: " + str(dt.now())) + # Construct Email Content email_message = EmailMessage() - email_message.add_header('To', ', '.join(to_emails)) - email_message.add_header('From', username) - email_message.add_header('Subject', 'Checkin') + email_message['To'] = ', '.join(config.dev_emails) + email_message['from'] = config.username + email_message['Subject'] = 'Checkin' email_message.add_header('X-Priority', '1') # Urgency, 1 highest, 5 lowest - email_message.set_content('Still Alive :)') + message = "still Alive :) " + email_message.set_content(message) + # Server sends email message - smtp_server = smtp_setup(username, password, host) - server = smtp_server + server = smtp_setup(config.username, config.password, config.host) server.send_message(email_message) + logging.info("Checkin email sent.") + server.quit() diff --git a/cougarvision_utils/detect_img.py b/cougarvision_utils/detect_img.py index 7571154..8cd2c42 100644 --- a/cougarvision_utils/detect_img.py +++ b/cougarvision_utils/detect_img.py @@ -11,15 +11,14 @@ from io import BytesIO from datetime import datetime as dt -import re import os +import logging from PIL import Image -from animl import classification, split -from animl import detection +import animl from sageranger import is_target, attach_image, post_event from cougarvision_utils.cropping import draw_bounding_box_on_image -from cougarvision_utils.alert import smtp_setup, send_alert +from cougarvision_utils.alert import send_alert def detect(images, config): # pylint: disable=too-many-locals @@ -47,7 +46,7 @@ def detect(images, config): # pylint: disable=too-many-locals # confidendce and checkpoint frequency conf = config.confidence ch_f = config.checkpoint_frequency - results = detection.detect(config.detector_model_load, + results = animl.detect(config.detector_model_load, image_path_list, resize_width=1280, resize_height=1280, @@ -56,7 +55,7 @@ def detect(images, config): # pylint: disable=too-many-locals batch_size=4 ) # Parse results - data_frame = detection.parse_detections(results) + data_frame = animl.parse_detections(results) # single classification function checks for the file # extension so we add it data_frame["extension"] = data_frame["filepath"].str.extract( @@ -64,18 +63,18 @@ def detect(images, config): # pylint: disable=too-many-locals expand=False).str.lower() # filter out all non animal detections if not data_frame.empty: - animal_df = split.get_animals(data_frame) - # other_df = split.get_empty(data_frame) + animal_df = animl.get_animals(data_frame) + # run classifier on animal detections if there are any if not animal_df.empty: classifer_model = config.classifier_model_load - predictions_raw = classification.classify(classifer_model, + predictions_raw = animl.classify(classifer_model, animal_df, batch_size=4 ) # single classification expects a list class_list_series = config.class_list["species"].tolist() - preds = classification.single_classification(animal_df, + preds = animl.single_classification(animal_df, None, predictions_raw, class_list_series @@ -87,8 +86,10 @@ def detect(images, config): # pylint: disable=too-many-locals cougars = cougars.reset_index(drop=True) # create a row in the dataframe containing only the camera name # flake8: disable-next - cougars['cam_name'] = cougars['filepath'].apply( - lambda x: re.findall(r'[A-Z]\d+', x)[0]) + # print("######Cougars:",cougars) + #cougars['cam_name'] = cougars['filepath'].apply( + # lambda x: re.findall(r'[A-Z]\d+', x)[0]) + cougars["cam_name"] = cougars["filepath"].str.extract(r"([A-Z]\d+)") # Sends alert for each cougar detection for idx in range(len(cougars.index)): label = cougars.at[idx, 'prediction'] @@ -127,34 +128,43 @@ def detect(images, config): # pylint: disable=too-many-locals cam_name = cougars.at[idx, 'cam_name'] er_alerts = config.er_alerts if label in config.alert_targets and er_alerts is True: - is_target(cam_name, - config.authorization, label) + try: + is_target(cam_name, + config.authorization, + label) + except KeyError as e: + logging.warning("Invalid authorization token missing key: %s", str(e)) + print("Invalid authorization", + f"token missing key {e}") + # Email or Earthranger alerts as dictated in the config yml if config.er_alerts is True: - event_id = post_event(label, - cam_name, - config.authorization) - response = attach_image(event_id, - img_byte, - config.authorization, - label) - print(response) + try: + event_id = post_event(label, + cam_name, + config.authorization) + response = attach_image(event_id, + img_byte, + config.authorization, + label) + logging.info("Posted event on earthranger with associated img.") + print(response) + except KeyError as e: + logging.warning("Invalid authorization token missing key: %s", str(e)) + print("Invalid authorization.", + f"token missing key {e}") + if config.email_alerts is True: - smtp_server = smtp_setup(config.username, - config.password, - config.host - ) dev = 0 - send_alert(label, image_bytes, smtp_server, - config.username, config.consumer_emails, - dev, prob - ) + send_alert(config,label,image_bytes, + dev, prob) dev = 1 - send_alert(label, image_bytes, smtp_server, - config.username, config.dev_emails, + send_alert(config,label, image_bytes, dev, prob) # Write Dataframe to csv current_date = dt.now() formatted_dt = current_date.strftime("%m-%d-%Y_%H:%M:%S") - cougars.to_csv(f'{config.log_dir}dataframe_{formatted_dt}') + logs = config.log_dir + os.makedirs(logs, exist_ok=True) + cougars.to_csv(f'{logs}dataframe_{formatted_dt}') diff --git a/cougarvision_utils/get_images.py b/cougarvision_utils/get_images.py index bc9a403..2483255 100644 --- a/cougarvision_utils/get_images.py +++ b/cougarvision_utils/get_images.py @@ -10,6 +10,35 @@ last_id.txt as well, but it creates a new one if there is not one currently present. +Examples: + + request cameras + get list of camaras + request <- "cameras" + parameters <- "" + + recent photo count + request <- "photos/recent/count" + parameters <- "" + + get recent photos across cameras + request <- "photos/recent" + parameters <- "limit=100" + + 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) + request <- "photos" + parameters <- "page=1&sort_date=desc&camera_id[]= + 60272&date_start=2022-09-01&date_end=2022-10-07" + + get subscriptions + request <- "subscriptions" + parameters <- "" + """ import json @@ -19,36 +48,7 @@ import os import requests import numpy as np - -# pylint: disable=pointless-string-statement -""" -#request examples -#get list of camaras -request <- "cameras" -parameters <- "" - -recent photo count -request <- "photos/recent/count" -parameters <- "" - -get recent photos across cameras -request <- "photos/recent" -parameters <- "limit=100" - -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) -request <- "photos" -parameters <- "page=1&sort_date=desc&camera_id[]= -60272&date_start=2022-09-01&date_end=2022-10-07" - -get subscriptions -request <- "subscriptions" -parameters <- "" -""" +from datetime import datetime as dt def request_strikeforce(username, auth_token, base, request, parameters): @@ -84,16 +84,11 @@ def request_strikeforce(username, auth_token, base, request, parameters): return info except requests.exceptions.ConnectionError as excpt: - logging.warning("Failed to connect attempt: %s error %s", - {attempt + 1}, - {excpt}) + logging.warning( "Failed to connect to StrikeForce:%s", str(excpt)) print(f'Connection Error {attempt + 1}: {excpt}') time.sleep(15) # wait 15 seconds except requests.exceptions.Timeout as excpt: - logging.warning("Failed to connect to" - " StrikeForce attempt: %s error %s", - {attempt + 1}, - {excpt}) + logging.warning( "Timeout error strikeforce: %s", str(excpt)) print(f'Timeout Error {attempt + 1}: {excpt}') time.sleep(15) # wait 15 seconds @@ -122,7 +117,7 @@ def fetch_image_api(config): # pylint: disable=too-many-locals # id_path is the path to the id text file path = config.id_path # try creating file throw exception if it - # does not exist + # does not existdatefmt='%Y-%m-%d %H:%M:%S' try: with open(path, "x", encoding="utf-8") as file: file.write(str(0)) # write first ID from sf @@ -152,7 +147,9 @@ def fetch_image_api(config): # pylint: disable=too-many-locals camera = config.camera_names[photo['relationships'] ['camera']['data']['id']] except KeyError: - logging.warning('skipped img: no associated cam ID') + id_camera = str(photo['id']) + logging.warning("skipped img: no associated cam ID for image: %s ", id_camera) + # id_camera, " at: " + str(dt.now())) continue image_dir = config.save_dir @@ -164,7 +161,13 @@ def fetch_image_api(config): # pylint: disable=too-many-locals newname += "_" + date_time newname += "_" + info['file_thumb_filename'] # native extension from strikeforce is .JPG.jpeg for some reason - stripped_name = newname.replace(".JPG.jpeg", ".jpg") + list_endings = [".JPG.jpeg", ".jpg.jpeg", ".jpeg", ".MP4.jpeg", ".AVI.jpeg"] + + for n in list_endings: + if n in newname: + stripped_name = newname.replace(str(n), ".jpg") + + # stripped_name = newname.replace(".JPG.jpeg", ".jpg") urllib.request.urlretrieve(info['file_thumb_url'], stripped_name) new_photos.append([photo['id'], info['file_thumb_url'], stripped_name]) @@ -172,9 +175,11 @@ def fetch_image_api(config): # pylint: disable=too-many-locals new_photos = np.array(new_photos) if len(new_photos) > 0: # update last image new_last = max(new_photos[:, 0]) + new_id = str(new_last) # write new id to .txt file with open(path, "w", encoding="utf-8") as file: file.writelines(new_id) return new_photos + \ No newline at end of file diff --git a/cougarvision_utils/get_info.py b/cougarvision_utils/get_info.py index 7873439..898c91f 100644 --- a/cougarvision_utils/get_info.py +++ b/cougarvision_utils/get_info.py @@ -3,8 +3,8 @@ Get_info holds the ConfigInfo data class that holds attribute values from the configuration file. It also hold the display_info class that has values from the config directly -related to the display file.Direct mapping is handled in fetch -and alert in get_config_info function.Fields defined in this class +related to the display file.Direct mapping is handled in sageranger +in get_config_info function.Fields defined in this class will be the values mapped.The field values must match exactly to the config file. Fetch_and_alert, get_images, display and detect_img rely on these attribute values. diff --git a/cougarvision_utils/strikeforcegetcameras.py b/cougarvision_utils/strikeforcegetcameras.py index 7e75184..083a784 100644 --- a/cougarvision_utils/strikeforcegetcameras.py +++ b/cougarvision_utils/strikeforcegetcameras.py @@ -2,9 +2,15 @@ import json import requests +BASE = "https://api.strikeforcewireless.com/api/v2/" +REQUEST = "cameras" +PARAMETERS = "" +USERNAME = "" +AUTH_TOKEN = "" + def get_data(base, request, parameters, username, authentication_token): - '''Function for retrieving camera info from strikeforce + """Function for retrieving camera info from strikeforce Args: base: strikeforce api link request: cameras @@ -13,7 +19,8 @@ def get_data(base, request, parameters, username, authentication_token): authentication_token: auth token obtained from strikeforceget.py Returns: - json with all camera info''' + 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, timeout=20) @@ -21,16 +28,36 @@ def get_data(base, request, parameters, username, authentication_token): return json.loads(data_response) -BASE = "https://api.strikeforcewireless.com/api/v2/" -REQUEST = "cameras" -PARAMETERS = "" -USERNAME = "" -AUTH_TOKEN = "" - -data = get_data(BASE, REQUEST, PARAMETERS, USERNAME, AUTH_TOKEN) -pretty_json = json.dumps(data, indent=4) -cameras = [] -list_of_cam_info = data['data'] -for _, i in enumerate(list_of_cam_info): - cameras = list_of_cam_info[i]['id'] - print(cameras) +def main(): + """Main + + This function parses the json response to retireve + the camera id and name and prints a dict of the values + mapped to the corresponding camera for the config value + 'camera_names' + """ + + data = get_data(BASE, REQUEST, PARAMETERS, USERNAME, AUTH_TOKEN) + cameras = [] + + # print(list(data.keys())) + # optional -- missing cam id for now + # list_of_cam_data = data["data"] + # for idx, d in enumerate(list_of_cam_data): + # last_synced = list_of_cam_data[idx]['attributes']['last_sync_time'] + + list_of_cam_info = data["included"] + + for idx, name in enumerate(list_of_cam_info): + if list_of_cam_info[idx]['attributes'].get('camera_id') is not None: + name = list_of_cam_info[idx]['attributes']['name'] + cam_id = list_of_cam_info[idx]['attributes']['camera_id'] + temp_tuple = cam_id, name + cameras.append(temp_tuple) + + camera_dict = dict(cameras) + print(camera_dict) + + +if __name__ == "__main__": + main() diff --git a/fetch_and_alert.py b/fetch_and_alert.py index a3a983b..d90baa5 100644 --- a/fetch_and_alert.py +++ b/fetch_and_alert.py @@ -18,16 +18,14 @@ """ # Import local utilities -import argparse import time import warnings from datetime import datetime as dt import logging -from dataclasses import fields import schedule -import yaml from sageranger.post_monthly import post_monthly_obs +from sageranger.unpack_info import get_config_info from cougarvision_utils.detect_img import detect from cougarvision_utils.alert import checkin from cougarvision_utils.get_images import fetch_image_api @@ -36,12 +34,14 @@ def logger(): """Function for creating log file""" - logging.basicConfig(filename='cougarvision.log', level=logging.INFO) + logging.basicConfig(filename='cougarvision.log', level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s') def fetch_detect_alert(config): """Function for fetching images, detection, and sending alerts""" # Run the scheduler + logging.info("Starting cougarvision.") print("Running fetch_and_alert") print("Fetching images") images = fetch_image_api(config) @@ -52,45 +52,8 @@ def fetch_detect_alert(config): print("Sleeping since: " + str(dt.now())) -def parse_args(): - """Creates parser for config yaml. - - This function creates an arguement parser that creates an - args container with the arguement 'CONFIG'. - - Returns: - argsparse.Namespace: An object containing all parsed arguement - values as attributes (e.g., args.CONFIG). - """ - parser = argparse.ArgumentParser(description='Retrieves images from \ - email & web scraper & runs detection') - parser.add_argument('CONFIG', type=str, help='Path to config file') - - return parser.parse_args() - - -def get_config_info(class_type): - """Parses through config file. - - This function maps values to the dataclasses - found in get_info. - - """ - args = parse_args() - config_path = args.CONFIG - - with open(config_path, 'r', encoding='utf-8') as file: - config_dict = yaml.safe_load(file) - - # for direct mapping only use fields in the class fields - valid_keys = {f.name for f in fields(class_type)} - filtered_keys = {k: v for k, v in config_dict.items() if k in valid_keys} - - return class_type(**filtered_keys) - - def main(): - '''Runs main program and schedules future runs''' + """Runs main program and schedules future runs""" # Numpy FutureWarnings from tensorflow import warnings.filterwarnings('ignore', category=FutureWarning) @@ -107,21 +70,19 @@ def main(): ).seconds.do(lambda: fetch_detect_alert(config)) else: - schedule.every(config.run_scheduler + schedule.every(3 ).minutes.do(lambda: fetch_detect_alert(config)) - schedule.every(config.checkin_interval).hours.do( - checkin, - config.dev_emails, - config.username, - config.password, - config.host - ) + schedule.every(config.checkin_interval + ).minutes.do(lambda: + checkin(config)) + if config.post_monthly: - schedule.every(30).days.do(post_monthly_obs, - config.authorization) - + schedule.every(30).days.do(lambda: post_monthly_obs( + config.authorization, + config.camera_names)) + while True: schedule.run_pending()