Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6e06433
Add try/except for sageranger function calls.
montse-jara Jun 16, 2026
aa06036
fix post monthly call,
montse-jara Jun 16, 2026
efa8c2d
fix pylint/flake8 errors and fix docstrings.
montse-jara Jun 16, 2026
0ba7247
fix flake8 errors.
montse-jara Jun 16, 2026
b74ef98
Fix checkin function call.
montse-jara Jun 26, 2026
498a630
Add log directory creation if it does not exist.
montse-jara Jun 26, 2026
b17fe6a
Change animl import to match latest update.
montse-jara Jun 26, 2026
ed1266e
Remove unused imports.
montse-jara Jun 26, 2026
e4950ba
Add missing username and password variables.
montse-jara Jun 26, 2026
9f9d587
Update function to print dictionary of cameras to camera id with adde…
montse-jara Jun 26, 2026
8a01ac7
Clean up comments.
montse-jara Jun 26, 2026
06faa6e
Add quit commands after sending emails.
montse-jara Jul 9, 2026
e3415e9
Remove incorrect line.
montse-jara Jul 9, 2026
feee681
Remove timestamp.
montse-jara Jul 9, 2026
2893db9
fix flake8 and pylint errors.
montse-jara Jul 9, 2026
254823d
Remove unpacking config value logic and import from sageranger.
montse-jara Jul 9, 2026
067cd20
Fix pylint and flake8 errors.
montse-jara Jul 9, 2026
d781064
Change findall to str.extract.
montse-jara Jul 9, 2026
b2a42e7
Update to print out a dict of camera to sf id.
montse-jara Jul 9, 2026
ecfb8e4
Clean up functions, include main, fix spacing errors.
montse-jara Jul 9, 2026
9354eee
Update the dataclass module docstring.
montse-jara Jul 9, 2026
7305004
Fix log directory creation.
montse-jara Jul 14, 2026
30761ce
Update alert email formatting and limit passed arguements.
montse-jara Jul 16, 2026
331c3c2
Change arguements passed to checkin.
montse-jara Jul 16, 2026
252a056
Change arguements passed to function alert.
montse-jara Jul 16, 2026
37f4267
Fix flake8 and pylint errors.
montse-jara Jul 16, 2026
6902eb8
Change float confidence to a string value.
montse-jara Jul 23, 2026
31476cb
Replace end of image name with jpg.
montse-jara Jul 30, 2026
08c8510
Add logging info to email notifcation and post observations.
montse-jara Jul 30, 2026
5bb7f0d
Add logging info for skipped images and timeout errors.
montse-jara Jul 30, 2026
8ef784b
Add logging to show when cougarvision starts.
montse-jara Jul 30, 2026
18544a8
Add logging info about emails.
montse-jara Aug 6, 2026
df80c23
Add logging info and fix function calls to send emails.
montse-jara Aug 6, 2026
2126ecf
Update logging statements.
montse-jara Aug 6, 2026
fc750b0
Update function calls and add logging stucture and info.
montse-jara Aug 11, 2026
c4761c3
Add missing parameter.
montse-jara Aug 11, 2026
b7ec76d
Add logging info and fix function calls missing parameters.
montse-jara Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions config/fetch_and_alert.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ detector_model_type: "<insert 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
Expand All @@ -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: <must get from web dev tools after strikeforcewireless login>
#save_dir - path to where the images get stored (must create folder)
Expand Down
88 changes: 52 additions & 36 deletions cougarvision_utils/alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,56 +8,60 @@
'''

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
smtp_server.login(username, password)
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"\
Expand All @@ -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()
76 changes: 43 additions & 33 deletions cougarvision_utils/detect_img.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -56,26 +55,26 @@ 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(
r'(\.[^.]+)$',
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
Expand All @@ -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']
Expand Down Expand Up @@ -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}')
Loading