diff --git a/config/cameratraps.yml b/config/cameratraps.yml deleted file mode 100644 index e12dfd4..0000000 --- a/config/cameratraps.yml +++ /dev/null @@ -1 +0,0 @@ -camera_traps_path: /home/ctl/CameraTraps \ No newline at end of file diff --git a/config/fetch_and_alert.yml b/config/fetch_and_alert.yml index a414ff8..ae34700 100644 --- a/config/fetch_and_alert.yml +++ b/config/fetch_and_alert.yml @@ -10,13 +10,13 @@ checkpoint_frequency: -1 #log_dir - path to logs (must create this folder first) log_dir: /home/johnsmith/cougarvision/logs/ #classes - path to the class list for the classifier model -classes: /home/johnsmith/cougarvision/labels/sw_classes.txt +class_list: /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: [] #threshold confidence score -confidence: 0.7 +confidence_threshold: 0.7 #cpu threads threads: 8 # Web Scraping @@ -44,3 +44,5 @@ admin: admin@email.com token: '' #authorization bearer token retreived from same earthranger interactive api example requests authorization: 'Bearer ' +#folder for data +camera_traps_path: /home/ctl/CameraTraps \ No newline at end of file diff --git a/cougarvision_utils/ImageCropGenerator.py b/cougarvision_utils/ImageCropGenerator.py deleted file mode 100644 index c042aff..0000000 --- a/cougarvision_utils/ImageCropGenerator.py +++ /dev/null @@ -1,126 +0,0 @@ -import numpy as np -from PIL import Image, ImageOps, ImageFile -from tensorflow.keras.utils import Sequence - -ImageFile.LOAD_TRUNCATED_IMAGES = True - - -def crop_generator(images, boxes, resize=299, buffer=0, batch_size=32, standardize=True): - """ - Yields the next training batch. - Suppose `samples` is an array [[image1_filename,label1], [image2_filename,label2],...]. - """ - num_samples = len(images) - batch_size = int(batch_size) - resize = int(resize) - # offset=0 - while True: # Loop forever so the generator never terminates - # shuffle(samples) - # Get index to start each batch: [0, batch_size, 2*batch_size, ..., max multiple of batch_size <= num_samples] - for offset in range(0, num_samples, batch_size): - # Initialise X_train and y_train arrays for this batch - X_eval = [] - - # For each image - for i in range(offset, min(num_samples, offset + batch_size)): - # Load image (X) and label (y) - try: - img = Image.open(images[i]) - except OSError: - continue - width, height = img.size - - left = width * boxes[i, 0] - top = height * boxes[i, 1] - right = width * (boxes[i, 0] + boxes[i, 2]) - bottom = height * (boxes[i, 1] + boxes[i, 3]) - - buffer2 = max(right - left, bottom - top) * buffer - - left = max(0, left - buffer2) - top = max(0, top - buffer2) - right = min(width, right + buffer2) - bottom = min(height, bottom + buffer2) - - img = img.crop((left, top, right, bottom)) - img = resize_with_padding(img, (resize, resize)) - # img=img.resize((resize,resize)) - - # apply any kind of preprocessing img = cv2.resize(img,(resize,resize)) - # Add example to arrays - if standardize: - X_eval.append(np.array(img) / 255) - else: - X_eval.append(np.array(img)) - - # Make sure they're numpy arrays (as opposed to lists) - X_eval = np.array(X_eval) - - # The generator-y part: yield the next training batch - yield (X_eval) - # offset+=batch_size - - -def resize_with_padding(img, expected_size): - # img.thumbnail((expected_size[0], expected_size[1])) - if img.size[0] == 0 or img.size[1] == 0: return img - if img.size[0] > img.size[1]: - new_size = (expected_size[0], int(expected_size[1] * img.size[1] / img.size[0])) - else: - new_size = (int(expected_size[0] * img.size[0] / img.size[1]), expected_size[1]) - img = img.resize(new_size, Image.BILINEAR) # NEAREST BILINEAR - # print(img.size) - delta_width = expected_size[0] - img.size[0] - delta_height = expected_size[1] - img.size[1] - pad_width = delta_width // 2 - pad_height = delta_height // 2 - padding = (pad_width, pad_height, delta_width - pad_width, delta_height - pad_height) - return ImageOps.expand(img, padding) - - -class GenerateCropsFromFile(Sequence): - def __init__(self, x, boxes, resize=299, buffer=2, batch_size=32, standardize=True): - self.x = x - self.boxes = boxes - self.resize = int(resize) - self.buffer = buffer - self.batch_size = int(batch_size) - self.standardize = standardize - - def __len__(self): - return int(np.ceil(len(self.x) / float(self.batch_size))) - - def __getitem__(self, idx): - imgarray = [] - for i in range(min(len(self.x), idx * self.batch_size), min(len(self.x), (idx + 1) * self.batch_size)): - # for i in range(idx * self.batch_size,(idx + 1) *self.batch_size): - # for i in range(1,2): - try: - img = Image.open(self.x[i]) - except OSError: - continue - width, height = img.size - - left = width * self.boxes[i, 0] - top = height * self.boxes[i, 1] - right = width * (self.boxes[i, 0] + self.boxes[i, 2]) - bottom = height * (self.boxes[i, 1] + self.boxes[i, 3]) - # print(left,right,top,bottom) - - buffer2 = max(right - left, bottom - top) * self.buffer - - left = max(0, left - buffer2) - top = max(0, top - buffer2) - right = min(width, right + buffer2) - bottom = min(height, bottom + buffer2) - - img = img.crop((left, top, right, bottom)) - img = resize_with_padding(img, (self.resize, self.resize)) - if self.standardize: - imgarray.append(np.array(img) / 255) - else: - imgarray.append(np.array(img)) - # img=np.array(img) - # imgarray.append(np.expand_dims(img, axis=0)/255) - - return np.array(imgarray) diff --git a/cougarvision_utils/cropping.py b/cougarvision_utils/cropping.py index 4cea58d..af14249 100644 --- a/cougarvision_utils/cropping.py +++ b/cougarvision_utils/cropping.py @@ -43,11 +43,7 @@ ] -def draw_bounding_box_on_image(image, - ymin, - xmin, - ymax, - xmax, +def draw_bounding_box_on_image(detection, clss=None, thickness=4, expansion=0, @@ -86,13 +82,24 @@ def draw_bounding_box_on_image(image, else: color = COLORS[int(clss) % len(COLORS)] - draw = ImageDraw.Draw(image) - im_width, im_height = image.size + label = detection['prediction'] + # uncomment this line to use conf value for dev email alert + prob = str(detection['conf']) + + img = Image.open(detection['file']) + + x_min = detection['bbox_x'] + y_min = detection['bbox_y'] + x_max = detection['bbox_x'] + detection['bbox_width'] + y_max = detection['bbox_y'] + detection['bbox_height'] + + draw = ImageDraw.Draw(img) + im_width, im_height = img.size if use_normalized_coordinates: - (left, right, top, bottom) = (xmin * im_width, xmax * im_width, - ymin * im_height, ymax * im_height) + (left, right, top, bottom) = (x_min * im_width, x_max * im_width, + y_min * im_height, y_max * im_height) else: - (left, right, top, bottom) = (xmin, xmax, ymin, ymax) + (left, right, top, bottom) = (x_min, x_max, y_min, y_max) if expansion > 0: left -= expansion diff --git a/cougarvision_utils/detect_img.py b/cougarvision_utils/detect_img.py index 892a36e..73a46ba 100644 --- a/cougarvision_utils/detect_img.py +++ b/cougarvision_utils/detect_img.py @@ -8,26 +8,17 @@ post_event_er.py, and attach_image_er.py as well as some functions that must be imported from animl. ''' - from io import BytesIO from datetime import datetime as dt import re -import sys -import yaml -from PIL import Image -from animl import parse_results, classify, split + +import animl from sageranger import is_target, attach_image, post_event -from animl.detectMD import detect_MD_batch -from cougarvision_utils.cropping import draw_bounding_box_on_image +#from cougarvision_utils.cropping import draw_bounding_box_on_image from cougarvision_utils.alert import smtp_setup, send_alert -with open("config/cameratraps.yml", 'r') as stream: - CAM_CONFIG = yaml.safe_load(stream) - sys.path.append(CAM_CONFIG['camera_traps_path']) - - def detect(images, config, c_model, d_model): ''' This function takes in a dataframe of images and runs a detector model, @@ -41,85 +32,63 @@ 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 ''' - email_alerts = bool(config['email_alerts']) - er_alerts = bool(config['er_alerts']) log_dir = config['log_dir'] - checkpoint_f = config['checkpoint_frequency'] - confidence = config['confidence'] - classes = config['classes'] + + classes = animl.load_class_list(config['class_list'])['class'] targets = config['alert_targets'] - username = config['username'] - password = config['password'] - consumer_emails = config['consumer_emails'] - dev_emails = config['dev_emails'] - host = 'imap.gmail.com' - token = config['token'] - authorization = config['authorization'] + confidence_threshold = config['confidence_threshold'] + if len(images) > 0: - # extract paths from dataframe - image_paths = images[:, 2] # 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 = animl.detect(d_model, + images, + 1280, 1280, + checkpoint_path=None, + confidence_threshold=confidence_threshold, + checkpoint_frequency=config['checkpoint_frequency'], + batch_size=4) # Parse results - data_frame = parse_results.from_MD(results, None, None) + data_frame = animl.parse_detections(results) # 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 = animl.get_animals(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) + predictions = animl.classify(c_model, animal_df, + resize_height=299, resize_width=299, + batch_size=4) # Parse results - max_df = parse_results.from_classifier(animal_df, - predictions, - classes, - None) + max_df = animl.single_classification(animal_df, None, predictions, classes,) # Creates a data frame with all relevant data cougars = max_df[max_df['prediction'].isin(targets)] # drops all detections with confidence less than threshold - cougars = cougars[cougars['conf'] >= confidence] + cougars = cougars[cougars['conf'] >= confidence_threshold] # 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 # 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']) - draw_bounding_box_on_image(img, - cougars.at[idx, 'bbox2'], - cougars.at[idx, 'bbox1'], - cougars.at[idx, - 'bbox2'] + - cougars.at[idx, - 'bbox4'], - cougars.at[idx, - 'bbox1'] + - cougars.at[idx, - 'bbox3'], - expansion=0, - use_normalized_coordinates=True) + for i, row in cougars.iterrows(): + # draw bounding box on image + #img = draw_bounding_box_on_image(row, expansion=0, use_normalized_coordinates=True) + img = animl.plot_box(row, file_col='file', prediction=True) + # save image image_bytes = BytesIO() img.save(image_bytes, format="JPEG") img_byte = image_bytes.getvalue() - cam_name = cougars.at[idx, 'cam_name'] - if label in targets and er_alerts is True: + + label = row['prediction'] + prob = str(row['conf']) + cam_name = row['cam_name'] + # connect to earthranger + if bool(config['er_alerts']): + token = config['token'] + authorization = config['authorization'] + is_target(cam_name, token, authorization, label) - # Email or Earthranger alerts as dictated in the config yml - if er_alerts is True: + event_id = post_event(label, cam_name, token, @@ -130,7 +99,14 @@ def detect(images, config, c_model, d_model): authorization, label) print(response) - if email_alerts is True: + # email alerts + if bool(config['email_alerts']): + username = config['username'] + password = config['password'] + host = 'imap.gmail.com' + consumer_emails = config['consumer_emails'] + dev_emails = config['dev_emails'] + smtp_server = smtp_setup(username, password, host) dev = 0 send_alert(label, image_bytes, smtp_server, @@ -138,6 +114,7 @@ def detect(images, config, c_model, d_model): dev = 1 send_alert(label, image_bytes, smtp_server, username, dev_emails, dev, prob) + # Write Dataframe to csv date = "%m-%d-%Y_%H:%M:%S" cougars.to_csv(f'{log_dir}dataframe_{dt.now().strftime(date)}') diff --git a/cougarvision_utils/log_utils.py b/cougarvision_utils/log_utils.py index 50d05dc..c71d813 100644 --- a/cougarvision_utils/log_utils.py +++ b/cougarvision_utils/log_utils.py @@ -1,7 +1,6 @@ import csv -import os.path - import yaml +import logging with open("config/fetch_and_alert.yml", 'r') as stream: config = yaml.safe_load(stream) diff --git a/fetch_and_alert.py b/fetch_and_alert.py index 9798d74..b320cdb 100644 --- a/fetch_and_alert.py +++ b/fetch_and_alert.py @@ -30,38 +30,13 @@ 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 + +import animl # Numpy FutureWarnings from tensorflow import warnings.filterwarnings('ignore', category=FutureWarning) # Parse arguments -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 -# Load Configuration Settings from YML file -with open(CONFIG_FILE, 'r', encoding='utf-8') as stream: - CONFIG = yaml.safe_load(stream) -# Set Email Variables for fetching -USERNAME = CONFIG['username'] -PASSWORD = CONFIG['password'] -TOKEN = CONFIG['token'] -AUTH = CONFIG['authorization'] -CLASSIFIER = CONFIG['classifier_model'] -DETECTOR = CONFIG['detector_model'] -DEV_EMAILS = CONFIG['dev_emails'] -HOST = 'imap.gmail.com' - - -# Set interval for checking in -CHECKIN_INTERVAL = CONFIG['checkin_interval'] - -# load models once -CLASSIFIER_MODEL = load_classifier(CLASSIFIER) -DETECTOR_MODEL = megadetector.MegaDetector(DETECTOR) def logger(): @@ -69,7 +44,7 @@ def logger(): logging.basicConfig(filename='cougarvision.log', level=logging.INFO) -def fetch_detect_alert(): +def fetch_detect_alert(CONFIG, CLASSIFIER_MODEL, DETECTOR_MODEL): '''Functions for fetching images, detection, and sending alerts''' # Run the scheduler print("Running fetch_and_alert") @@ -82,14 +57,26 @@ def fetch_detect_alert(): print("Sleeping since: " + str(dt.now())) -def main(): +def main(CONFIG): ''''Runs main program and schedules future runs''' + # Set Email Variables for fetching logger() - fetch_detect_alert() - schedule.every(10).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) + + # load models once + CLASSIFIER_MODEL = animl.load_classifier(CONFIG['classifier_model']) + DETECTOR_MODEL = animl.load_detector( CONFIG['detector_model'], model_type='mdv5') + + # run fetch at start + fetch_detect_alert(CONFIG, CLASSIFIER_MODEL, DETECTOR_MODEL) + + # Schedule future fetches + schedule.every(10).minutes.do(fetch_detect_alert, CONFIG, CLASSIFIER_MODEL, DETECTOR_MODEL) + schedule.every(CONFIG['checkin_interval']).hours.do(checkin, + CONFIG['dev_emails'], + CONFIG['username'], + CONFIG['password'], + 'imap.gmail.com') + schedule.every(30).days.do(post_monthly_obs, CONFIG['token'], CONFIG['authorization']) while True: schedule.run_pending() @@ -97,4 +84,13 @@ def main(): if __name__ == "__main__": - main() + 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 + # Load Configuration Settings from YML file + with open(config_file, 'r', encoding='utf-8') as stream: + CONFIG = yaml.safe_load(stream) + + main(CONFIG) diff --git a/mlgpuenv.yml b/mlgpuenv.yml deleted file mode 100644 index 55d494f..0000000 --- a/mlgpuenv.yml +++ /dev/null @@ -1,156 +0,0 @@ -name: mlgpu -channels: - - defaults -dependencies: - - _libgcc_mutex=0.1=main - - _openmp_mutex=4.5=1_gnu - - _tflow_select=2.1.0=gpu - - absl-py=0.13.0=py39h06a4308_0 - - aiohttp=3.8.1=py39h7f8727e_0 - - aiosignal=1.2.0=pyhd3eb1b0_0 - - astor=0.8.1=py39h06a4308_0 - - astunparse=1.6.3=py_0 - - async-timeout=4.0.1=pyhd3eb1b0_0 - - attrs=21.2.0=pyhd3eb1b0_0 - - blas=1.0=mkl - - blinker=1.4=py39h06a4308_0 - - bottleneck=1.3.2=py39hdd57654_1 - - brotli=1.0.9=he6710b0_2 - - brotlipy=0.7.0=py39h27cfd23_1003 - - c-ares=1.17.1=h27cfd23_0 - - ca-certificates=2022.4.26=h06a4308_0 - - cachetools=4.2.2=pyhd3eb1b0_0 - - certifi=2021.10.8=py39h06a4308_2 - - cffi=1.14.6=py39h400218f_0 - - charset-normalizer=2.0.4=pyhd3eb1b0_0 - - cryptography=3.4.8=py39hd23ed53_0 - - cudatoolkit=10.1.243=h6bb024c_0 - - cudnn=7.6.5=cuda10.1_0 - - cupti=10.1.168=0 - - cycler=0.11.0=pyhd3eb1b0_0 - - dataclasses=0.8=pyh6d0b6a4_7 - - dbus=1.13.18=hb2f20db_0 - - expat=2.4.4=h295c915_0 - - fontconfig=2.13.1=h6c09931_0 - - fonttools=4.25.0=pyhd3eb1b0_0 - - freetype=2.11.0=h70c0345_0 - - frozenlist=1.2.0=py39h7f8727e_0 - - gast=0.4.0=pyhd3eb1b0_0 - - giflib=5.2.1=h7b6447c_0 - - glib=2.69.1=h4ff587b_1 - - google-auth=1.33.0=pyhd3eb1b0_0 - - google-auth-oauthlib=0.4.4=pyhd3eb1b0_0 - - google-pasta=0.2.0=pyhd3eb1b0_0 - - grpcio=1.42.0=py39hce63b2e_0 - - gst-plugins-base=1.14.0=h8213a91_2 - - gstreamer=1.14.0=h28cd5cc_2 - - h5py=2.10.0=py39hec9cf62_0 - - hdf5=1.10.6=hb1b8bf9_0 - - humanfriendly=10.0=py39h06a4308_0 - - icu=58.2=he6710b0_3 - - importlib-metadata=4.11.3=py39h06a4308_0 - - importlib_metadata=4.11.3=hd3eb1b0_0 - - intel-openmp=2021.4.0=h06a4308_3561 - - jpeg=9d=h7f8727e_0 - - jsonpickle=2.0.0=pyhd3eb1b0_0 - - keras-preprocessing=1.1.2=pyhd3eb1b0_0 - - kiwisolver=1.3.2=py39h295c915_0 - - lcms2=2.12=h3be6417_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 - - libpng=1.6.37=hbc83047_0 - - libprotobuf=3.17.2=h4ff587b_1 - - libstdcxx-ng=9.3.0=hd4cf53a_17 - - libtiff=4.2.0=h85742a9_0 - - libuuid=1.0.3=h7f8727e_2 - - libwebp=1.2.0=h89dd481_0 - - libwebp-base=1.2.0=h27cfd23_0 - - libxcb=1.14=h7b6447c_0 - - libxml2=2.9.12=h03d6c58_0 - - lz4-c=1.9.3=h295c915_1 - - markdown=3.3.4=py39h06a4308_0 - - matplotlib=3.5.1=py39h06a4308_1 - - matplotlib-base=3.5.1=py39ha18d171_1 - - mkl=2021.4.0=h06a4308_640 - - mkl-service=2.4.0=py39h7f8727e_0 - - mkl_fft=1.3.1=py39hd3c417c_0 - - mkl_random=1.2.2=py39h51133e4_0 - - multidict=5.1.0=py39h27cfd23_2 - - munkres=1.1.4=py_0 - - ncurses=6.3=h7f8727e_2 - - numexpr=2.7.3=py39h22e1b3c_1 - - numpy=1.21.2=py39h20f2e39_0 - - numpy-base=1.21.2=py39h79a1101_0 - - oauthlib=3.1.1=pyhd3eb1b0_0 - - olefile=0.46=pyhd3eb1b0_0 - - openssl=1.1.1o=h7f8727e_0 - - opt_einsum=3.3.0=pyhd3eb1b0_1 - - packaging=21.3=pyhd3eb1b0_0 - - pandas=1.3.4=py39h8c16a72_0 - - pcre=8.45=h295c915_0 - - pillow=8.4.0=py39h5aabda8_0 - - pip=21.2.4=py39h06a4308_0 - - protobuf=3.17.2=py39h295c915_0 - - pyasn1=0.4.8=pyhd3eb1b0_0 - - pyasn1-modules=0.2.8=py_0 - - pycparser=2.21=pyhd3eb1b0_0 - - pyjwt=2.1.0=py39h06a4308_0 - - pyopenssl=21.0.0=pyhd3eb1b0_1 - - pyparsing=3.0.4=pyhd3eb1b0_0 - - pyqt=5.9.2=py39h2531618_6 - - pysocks=1.7.1=py39h06a4308_0 - - python=3.9.7=h12debd9_1 - - python-dateutil=2.8.2=pyhd3eb1b0_0 - - python-flatbuffers=2.0=pyhd3eb1b0_0 - - pytz=2021.3=pyhd3eb1b0_0 - - qt=5.9.7=h5867ecd_1 - - readline=8.1=h27cfd23_0 - - requests-oauthlib=1.3.0=py_0 - - rsa=4.7.2=pyhd3eb1b0_1 - - scipy=1.7.1=py39h292c36d_2 - - setuptools=58.0.4=py39h06a4308_0 - - sip=4.19.13=py39h295c915_0 - - six=1.16.0=pyhd3eb1b0_0 - - sqlite=3.36.0=hc218d9a_0 - - tensorboard=2.4.0=pyhc547734_0 - - tensorboard-plugin-wit=1.6.0=py_0 - - tensorflow=2.4.1=gpu_py39h8236f22_0 - - tensorflow-base=2.4.1=gpu_py39h29c2da4_0 - - tensorflow-estimator=2.6.0=pyh7b7c402_0 - - tensorflow-gpu=2.4.1=h30adc30_0 - - termcolor=1.1.0=py39h06a4308_1 - - tk=8.6.11=h1ccaba5_0 - - tornado=6.1=py39h27cfd23_0 - - tqdm=4.64.0=py39h06a4308_0 - - typing-extensions=3.10.0.2=hd3eb1b0_0 - - typing_extensions=3.10.0.2=pyh06a4308_0 - - tzdata=2021e=hda174b7_0 - - urllib3=1.26.7=pyhd3eb1b0_0 - - werkzeug=2.0.2=pyhd3eb1b0_0 - - wheel=0.37.0=pyhd3eb1b0_1 - - wrapt=1.13.3=py39h7f8727e_2 - - xz=5.2.5=h7b6447c_0 - - yarl=1.6.3=py39h27cfd23_0 - - zipp=3.6.0=pyhd3eb1b0_0 - - zlib=1.2.11=h7f8727e_4 - - zstd=1.4.9=haebb681_0 - - pip: - - chardet==4.0.0 - - click==7.0 - - future==0.18.2 - - humanize==1.0.0 - - idna==2.10 - - imbox==0.9.8 - - panoptes-client==1.4.0 - - panoptescli==1.1.3 - - pathvalidate==0.29.1 - - python-magic==0.4.24 - - pyyaml==5.3.1 - - redo==2.0.4 - - requests==2.25.1 - - schedule==1.1.0 -prefix: /home/kyra/anaconda3/envs/mlgpu