From fc3fc3e1543b431864e4da6da3be7ebe2ef93259 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Wed, 14 Jan 2026 15:01:12 -0800 Subject: [PATCH 01/12] python3.12 updates to conda env most of the packages were dependent on 3.8 python, and had specific wheels for that python version. the whole environment creation needed to be changed in order to be compatible with python3.12 which is what the newest animl version needs to run. this might need further updates but currently all the packages allow the environment to be created without conflicts --- cougarvision_env.yml | 150 ++++++++----------------------------------- 1 file changed, 25 insertions(+), 125 deletions(-) diff --git a/cougarvision_env.yml b/cougarvision_env.yml index 2f5df9f..82fd791 100644 --- a/cougarvision_env.yml +++ b/cougarvision_env.yml @@ -1,129 +1,29 @@ -name: cougarvision +name: cougarvision_updated channels: - conda-forge - - defaults + - nodefaults dependencies: - - _libgcc_mutex=0.1=main - - _openmp_mutex=4.5=1_gnu - - _tflow_select=2.1.0=gpu - - absl-py=0.15.0=pyhd3eb1b0_0 - - aiohttp=3.8.1=py38h7f8727e_0 - - aiosignal=1.2.0=pyhd3eb1b0_0 - - astor=0.8.1=py38h06a4308_0 - - astunparse=1.6.3=py_0 - - async-timeout=4.0.1=pyhd3eb1b0_0 - - attrs=21.4.0=pyhd3eb1b0_0 - - blas=1.0=mkl - - blinker=1.4=py38h06a4308_0 - - brotlipy=0.7.0=py38h27cfd23_1003 - - c-ares=1.18.1=h7f8727e_0 - - ca-certificates=2022.3.29=h06a4308_0 - - cachetools=4.2.2=pyhd3eb1b0_0 - - certifi=2021.10.8=py38h06a4308_2 - - cffi=1.15.0=py38hd667e15_1 - - charset-normalizer=2.0.4=pyhd3eb1b0_0 - - click=8.0.4=py38h06a4308_0 - - cryptography=3.4.8=py38hd23ed53_0 - - cudatoolkit=10.1.243=h6bb024c_0 - - cudnn=7.6.5=cuda10.1_0 - - cupti=10.1.168=0 - - dataclasses=0.8=pyh6d0b6a4_7 - - frozenlist=1.2.0=py38h7f8727e_0 - - gast=0.4.0=pyhd3eb1b0_0 - - google-pasta=0.2.0=pyhd3eb1b0_0 - - hdf5=1.10.6=hb1b8bf9_0 - - idna=3.3=pyhd3eb1b0_0 - - importlib-metadata=4.8.2=py38h06a4308_0 - - intel-openmp=2021.4.0=h06a4308_3561 - - keras-preprocessing=1.1.2=pyhd8ed1ab_0 - - ld_impl_linux-64=2.35.1=h7274673_9 - - libffi=3.3=he6710b0_2 - - libgcc-ng=9.3.0=h5101ec6_17 - - libgfortran-ng=7.5.0=ha8ba4b0_17 - - libgfortran4=7.5.0=ha8ba4b0_17 - - libgomp=9.3.0=h5101ec6_17 - - libprotobuf=3.19.1=h4ff587b_0 - - libstdcxx-ng=9.3.0=hd4cf53a_17 - - markdown=3.3.4=py38h06a4308_0 - - mkl=2021.4.0=h06a4308_640 - - mkl-service=2.4.0=py38h7f8727e_0 - - mkl_fft=1.3.1=py38hd3c417c_0 - - mkl_random=1.2.2=py38h51133e4_0 - - multidict=5.2.0=py38h7f8727e_2 - - ncurses=6.3=h7f8727e_2 - - oauthlib=3.2.0=pyhd3eb1b0_0 - - openssl=1.1.1n=h7f8727e_0 - - opt_einsum=3.3.0=pyhd3eb1b0_1 - - pip=21.2.4=py38h06a4308_0 - - pyasn1=0.4.8=pyhd3eb1b0_0 - - pyasn1-modules=0.2.8=py_0 - - pycparser=2.21=pyhd3eb1b0_0 - - pyjwt=2.1.0=py38h06a4308_0 - - pyopenssl=21.0.0=pyhd3eb1b0_1 - - pysocks=1.7.1=py38h06a4308_0 - - python=3.8.12=h12debd9_0 - - python_abi=3.8=2_cp38 - - readline=8.1.2=h7f8727e_1 - - requests=2.27.1=pyhd3eb1b0_0 - - requests-oauthlib=1.3.0=py_0 - - rsa=4.7.2=pyhd3eb1b0_1 - - scipy=1.7.3=py38hc147768_0 - - selenium=3.141.0=py38h27cfd23_1000 - - setuptools=58.0.4=py38h06a4308_0 - - sqlite=3.37.2=hc218d9a_0 - - tensorboard-plugin-wit=1.6.0=py_0 - - tensorflow-estimator=2.6.0=pyh7b7c402_0 - - tensorflow-gpu=2.4.1=h30adc30_0 - - termcolor=1.1.0=py38h06a4308_1 - - tk=8.6.11=h1ccaba5_0 - - urllib3=1.26.8=pyhd3eb1b0_0 - - werkzeug=2.0.3=pyhd3eb1b0_0 - - wheel=0.37.1=pyhd3eb1b0_0 - - xz=5.2.5=h7b6447c_0 - - yaml=0.2.5=h516909a_0 - - yarl=1.6.3=py38h27cfd23_0 - - zipp=3.7.0=pyhd3eb1b0_0 - - zlib=1.2.11=h7f8727e_4 + - python=3.12 + - pip + - setuptools + - wheel + - numpy>=2.0.2 + - pandas>=2.2.2 + - scikit-learn>=1.5.2 + - pillow>=11.0.0 + - tqdm>=4.66.5 + - dill>=0.4.0 + - openssl + - sqlite + - zlib - pip: - - chardet==4.0.0 - - clang==5.0 - - datetime==5.2 - - flatbuffers==1.12 - - fonttools==4.29.1 - - google-auth==2.22.0 - - google-auth-oauthlib==1.0.0 - - grpcio==1.56.2 - - h5py==3.1.0 - - humanfriendly==10.0 - - imbox==0.9.8 - - jsonpickle==2.1.0 - - keras==2.6.0 - - keras-applications==1.0.2 - - kiwisolver==1.3.2 - - matplotlib==3.5.1 - - mmdnn==0.3.1 - - numpy==1.19.5 - - opencv-python==4.8.0.74 - - packaging==21.3 - - pandas==1.2.4 - - pillow==9.0.1 - - protobuf==4.23.4 - - pyparsing==3.0.7 - - python-dateutil==2.8.2 - - pytz==2023.3 - - pyyaml==6.0 - - sageranger==1.0.3 - - schedule==1.1.0 - - six==1.15.0 - - tensorboard==2.13.0 - - tensorboard-data-server==0.7.1 - - tensorflow==2.6.0 - - torch==1.10.2 - - torchvision==0.11.3 - - tqdm==4.63.0 - - typing-extensions==3.7.4.3 - - tzdata==2023.3 - - webdriver-manager==3.5.4 - - wrapt==1.12.1 - - zope-interface==6.0 -prefix: /home/katie/anaconda3/envs/cougarvision + - --extra-index-url https://download.pytorch.org/whl/cu124 + - torch>=2.6.0 + - torchvision>=0.21.0 + - onnxruntime-gpu>=1.23.2 + - opencv-python>=4.12.0.88 + - timm>=1.0.9 + - ultralytics>=8.3.95 + - wget>=3.2 + - schedule>=1.2.2 + From fd549e447baae2aa60b50e4370f9c9e08fe22086 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Wed, 14 Jan 2026 15:14:48 -0800 Subject: [PATCH 02/12] add sageranger dependency back to env creator --- cougarvision_env.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cougarvision_env.yml b/cougarvision_env.yml index 82fd791..7b0d119 100644 --- a/cougarvision_env.yml +++ b/cougarvision_env.yml @@ -26,4 +26,4 @@ dependencies: - ultralytics>=8.3.95 - wget>=3.2 - schedule>=1.2.2 - + - sageranger>=1.0.3 From d823f76cdb773529b36dbdd6eb1bd8a4d3a73bed Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Wed, 14 Jan 2026 15:27:28 -0800 Subject: [PATCH 03/12] update detector and classifier loading to match animl3.1.1 new function names in animl and new config values needed to load detectors --- config/fetch_and_alert.yml | 2 ++ fetch_and_alert.py | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/config/fetch_and_alert.yml b/config/fetch_and_alert.yml index a414ff8..73db228 100644 --- a/config/fetch_and_alert.yml +++ b/config/fetch_and_alert.yml @@ -4,6 +4,8 @@ email_alerts: True er_alerts: True #detector_model - path to detector model detector_model: /home/johnsmith/cougarvision/detector_models/megadetector.pt +#type of detection model expected ["MDV5", "MDV6", "YOLO", "ONNX"] +detector_model_type: " Date: Wed, 14 Jan 2026 16:09:05 -0800 Subject: [PATCH 04/12] update image grab and running detector images were not seen as images in animl because strikeforce sends a duplicate file extension, so now the extensions are modified to have one. the new detector loader also needs resizes set, and the defaults from animl are hardcoded now as the resizing --- cougarvision_utils/detect_img.py | 26 ++++++++++++++------------ cougarvision_utils/get_images.py | 5 +++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/cougarvision_utils/detect_img.py b/cougarvision_utils/detect_img.py index 892a36e..90467d9 100644 --- a/cougarvision_utils/detect_img.py +++ b/cougarvision_utils/detect_img.py @@ -15,9 +15,9 @@ import sys import yaml from PIL import Image -from animl import parse_results, classify, split +from animl import classification, split from sageranger import is_target, attach_image, post_event -from animl.detectMD import detect_MD_batch +from animl import detection from cougarvision_utils.cropping import draw_bounding_box_on_image from cougarvision_utils.alert import smtp_setup, send_alert @@ -58,15 +58,17 @@ def detect(images, config, c_model, d_model): if len(images) > 0: # extract paths from dataframe image_paths = images[:, 2] + image_path_list = image_paths.tolist() # Run Detection - results = detect_MD_batch(d_model, - image_paths, - checkpoint_path=None, - confidence_threshold=confidence, - checkpoint_frequency=checkpoint_f, - results=None, - quiet=False, - image_size=None) + results = detection.detect(d_model, + image_path_list, + resize_width=1280, + resize_height=1280, + confidence_threshold=confidence, + checkpoint_frequency=checkpoint_f, + batch_size=4 + ) + print(results) # Parse results data_frame = parse_results.from_MD(results, None, None) # filter out all non animal detections @@ -76,8 +78,8 @@ def detect(images, config, c_model, d_model): # 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) + predictions = classification.classify(c_model, animal_df, + batch_size=4) # Parse results max_df = parse_results.from_classifier(animal_df, predictions, diff --git a/cougarvision_utils/get_images.py b/cougarvision_utils/get_images.py index d5428a8..e0813f1 100644 --- a/cougarvision_utils/get_images.py +++ b/cougarvision_utils/get_images.py @@ -129,9 +129,10 @@ def fetch_image_api(config): newname = config['save_dir'] + camera newname += "_" + info['file_thumb_filename'] print(newname) - urllib.request.urlretrieve(info['file_thumb_url'], newname) + stripped_name = newname.replace(".JPG.jpeg", ".jpg") + urllib.request.urlretrieve(info['file_thumb_url'], stripped_name) new_photos.append([photos[i]['id'], - info['file_thumb_url'], newname]) + info['file_thumb_url'], stripped_name]) new_photos = np.array(new_photos) if len(new_photos) > 0: # update last image From 9d8d32cede222901d351aa2d1d055d94bfdbbc8f Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Thu, 15 Jan 2026 14:45:56 -0800 Subject: [PATCH 05/12] update classification and parsing to match animl update a few changes, loading the model passes a tuple that needs to be unpacked in fetch_and_alert. detect image had to rework the functions and what gets passed, theres some differences in type of data in some of the functions, so some stuff needs to be converted before its passed or have columns added to the df. but classification officially works. just need to test and make sure when it gets a positiive detection that it will send email properly. --- cougarvision_utils/detect_img.py | 44 +++++++++++++++++--------------- fetch_and_alert.py | 5 ++-- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/cougarvision_utils/detect_img.py b/cougarvision_utils/detect_img.py index 90467d9..7dcec29 100644 --- a/cougarvision_utils/detect_img.py +++ b/cougarvision_utils/detect_img.py @@ -28,7 +28,7 @@ sys.path.append(CAM_CONFIG['camera_traps_path']) -def detect(images, config, c_model, d_model): +def detect(images, config, c_model, d_model, class_list): ''' 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 @@ -46,7 +46,6 @@ def detect(images, config, c_model, d_model): log_dir = config['log_dir'] checkpoint_f = config['checkpoint_frequency'] confidence = config['confidence'] - classes = config['classes'] targets = config['alert_targets'] username = config['username'] password = config['password'] @@ -54,10 +53,13 @@ def detect(images, config, c_model, d_model): dev_emails = config['dev_emails'] host = 'imap.gmail.com' token = config['token'] + classes = config['classes'] authorization = config['authorization'] + if len(images) > 0: # extract paths from dataframe image_paths = images[:, 2] + # detection.detect expects the image paths in a list image_path_list = image_paths.tolist() # Run Detection results = detection.detect(d_model, @@ -68,39 +70,39 @@ def detect(images, config, c_model, d_model): checkpoint_frequency=checkpoint_f, batch_size=4 ) - print(results) # Parse results - data_frame = parse_results.from_MD(results, None, None) + data_frame = detection.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.getAnimals(data_frame) - other_df = split.getEmpty(data_frame) + animal_df = split.get_animals(data_frame) + other_df = split.get_empty(data_frame) # run classifier on animal detections if there are any if not animal_df.empty: - # create generator for images - predictions = classification.classify(c_model, animal_df, - batch_size=4) - # 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)] + predictions_raw = classification.classify(c_model, + animal_df, + batch_size=4) + # single classification expects a list + class_list_for_series = class_list["species"].tolist() + preds = classification.single_classification(animals=animal_df, + empty=other_df, + predictions_raw=predictions_raw, + class_list=class_list_for_series) + cougars = preds[preds['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 # flake8: disable-next - cougars['cam_name'] = cougars['file'].apply(lambda x: re.findall(r'[A-Z]\d+', x)[0]) # noqa: E501 # pylint: disable-msg=line-too-long + cougars['cam_name'] = cougars['filepath'].apply(lambda x: re.findall(r'[A-Z]\d+', x)[0]) # noqa: E501 # pylint: disable-msg=line-too-long # Sends alert for each cougar detection 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'] - img = Image.open(cougars.at[idx, 'file']) + prob = str(cougars.at[idx, 'confidence']) + img = Image.open(cougars.at[idx, 'filepath']) draw_bounding_box_on_image(img, cougars.at[idx, 'bbox2'], cougars.at[idx, 'bbox1'], diff --git a/fetch_and_alert.py b/fetch_and_alert.py index c3e492b..3ca695f 100644 --- a/fetch_and_alert.py +++ b/fetch_and_alert.py @@ -61,10 +61,9 @@ CHECKIN_INTERVAL = CONFIG['checkin_interval'] # load models once -CLASSIFIER_MODEL = load_classifier(CLASSIFIER, CLASSES) +CLASSIFIER_MODEL, CLASS_LIST = load_classifier(CLASSIFIER, CLASSES) DETECTOR_MODEL = load_detector(DETECTOR, MODEL_TYPE) - def logger(): '''Function for creating log file''' logging.basicConfig(filename='cougarvision.log', level=logging.INFO) @@ -78,7 +77,7 @@ def fetch_detect_alert(): images = fetch_image_api(CONFIG) print('Finished fetching images') print('Starting Detection') - detect(images, CONFIG, CLASSIFIER_MODEL, DETECTOR_MODEL) + detect(images, CONFIG, CLASSIFIER_MODEL, DETECTOR_MODEL, CLASS_LIST) print('Finished Detection') print("Sleeping since: " + str(dt.now())) From 4c3fa3f7fcd05c42ae266879fa4211701f719191 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Thu, 15 Jan 2026 14:51:52 -0800 Subject: [PATCH 06/12] added this to test forgot to remove --- cougarvision_utils/detect_img.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cougarvision_utils/detect_img.py b/cougarvision_utils/detect_img.py index 7dcec29..567037d 100644 --- a/cougarvision_utils/detect_img.py +++ b/cougarvision_utils/detect_img.py @@ -53,7 +53,6 @@ def detect(images, config, c_model, d_model, class_list): dev_emails = config['dev_emails'] host = 'imap.gmail.com' token = config['token'] - classes = config['classes'] authorization = config['authorization'] if len(images) > 0: From 8f9f007cb9ae58ad3377193c54ac93208f274309 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Thu, 15 Jan 2026 14:52:38 -0800 Subject: [PATCH 07/12] change env name from cougarvision_updated to _env animl-py now requires python3.12 as opposed to the previous env which needed 3.8, and all packages were very specifically for 3.8, so had to be entirely reworked to be for 3.12. --- cougarvision_env.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cougarvision_env.yml b/cougarvision_env.yml index 7b0d119..e35d8ef 100644 --- a/cougarvision_env.yml +++ b/cougarvision_env.yml @@ -1,4 +1,4 @@ -name: cougarvision_updated +name: cougarvision_env channels: - conda-forge - nodefaults From f5396e6fb6eb15f719849e51f32007867780e67c Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Thu, 15 Jan 2026 16:04:27 -0800 Subject: [PATCH 08/12] changed bbox to new names before bbox was bbox1 etc but now its bbox_x etc --- cougarvision_utils/detect_img.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cougarvision_utils/detect_img.py b/cougarvision_utils/detect_img.py index 567037d..01cbae6 100644 --- a/cougarvision_utils/detect_img.py +++ b/cougarvision_utils/detect_img.py @@ -103,16 +103,16 @@ def detect(images, config, c_model, d_model, class_list): prob = str(cougars.at[idx, 'confidence']) img = Image.open(cougars.at[idx, 'filepath']) draw_bounding_box_on_image(img, - cougars.at[idx, 'bbox2'], - cougars.at[idx, 'bbox1'], + cougars.at[idx, 'bbox_y'], + cougars.at[idx, 'bbox_x'], cougars.at[idx, - 'bbox2'] + + 'bbox_y'] + cougars.at[idx, - 'bbox4'], + 'bbox_h'], cougars.at[idx, - 'bbox1'] + + 'bbox_x'] + cougars.at[idx, - 'bbox3'], + 'bbox_w'], expansion=0, use_normalized_coordinates=True) image_bytes = BytesIO() From 786862dbc8db0a294744670409948d54019ee663 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Thu, 15 Jan 2026 16:05:51 -0800 Subject: [PATCH 09/12] remove print and add comment --- cougarvision_utils/get_images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cougarvision_utils/get_images.py b/cougarvision_utils/get_images.py index e0813f1..c96523e 100644 --- a/cougarvision_utils/get_images.py +++ b/cougarvision_utils/get_images.py @@ -128,7 +128,7 @@ def fetch_image_api(config): continue newname = config['save_dir'] + camera newname += "_" + info['file_thumb_filename'] - print(newname) + # native extension from strikeforce is .JPG.jpeg for some reason stripped_name = newname.replace(".JPG.jpeg", ".jpg") urllib.request.urlretrieve(info['file_thumb_url'], stripped_name) new_photos.append([photos[i]['id'], From 42aeb81c0737a5554440f3730b309ea635386279 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Thu, 15 Jan 2026 16:07:07 -0800 Subject: [PATCH 10/12] current cougarvision version is actually 1.1.0 --- cougarvision_utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cougarvision_utils/__init__.py b/cougarvision_utils/__init__.py index 4b4a921..1f9434d 100644 --- a/cougarvision_utils/__init__.py +++ b/cougarvision_utils/__init__.py @@ -1 +1 @@ -__version__ = "0.1.0.dev0" +__version__ = "1.1.1.dev0" From 892d8bffe61bc3abdf33380860dfd4ce57bb2901 Mon Sep 17 00:00:00 2001 From: Katie Garwood <115747639+kgarwoodsdzwa@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:25:57 -0800 Subject: [PATCH 11/12] Fix YAML syntax for detector_model_type --- 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 73db228..f4bc807 100644 --- a/config/fetch_and_alert.yml +++ b/config/fetch_and_alert.yml @@ -5,7 +5,7 @@ er_alerts: True #detector_model - path to detector model detector_model: /home/johnsmith/cougarvision/detector_models/megadetector.pt #type of detection model expected ["MDV5", "MDV6", "YOLO", "ONNX"] -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 From df6fc5e89d96cc168f801d641c6934bc390d7e69 Mon Sep 17 00:00:00 2001 From: Katie Garwood Date: Fri, 16 Jan 2026 10:46:21 -0800 Subject: [PATCH 12/12] update to animl 3.1.2 function fix to accept none in empty df --- cougarvision_utils/detect_img.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cougarvision_utils/detect_img.py b/cougarvision_utils/detect_img.py index 01cbae6..1d8720f 100644 --- a/cougarvision_utils/detect_img.py +++ b/cougarvision_utils/detect_img.py @@ -84,10 +84,11 @@ def detect(images, config, c_model, d_model, class_list): batch_size=4) # single classification expects a list class_list_for_series = class_list["species"].tolist() - preds = classification.single_classification(animals=animal_df, - empty=other_df, - predictions_raw=predictions_raw, - class_list=class_list_for_series) + preds = classification.single_classification(animal_df, + None, + predictions_raw, + class_list_for_series + ) cougars = preds[preds['prediction'].isin(targets)] # drops all detections with confidence less than threshold cougars = cougars[cougars['confidence'] >= confidence]