from PIL import Image from PIL import ExifTags import re import datetime import hashlib import logging import streamlit as st m_logger = logging.getLogger(__name__) # we can set the log level locally for funcs in this module #g_m_logger.setLevel(logging.DEBUG) m_logger.setLevel(logging.INFO) ''' A module to setup the input handling for the whale observation guidance tool both the UI elements (setup_input_UI) and the validation functions. ''' #allowed_image_types = ['webp'] allowed_image_types = ['jpg', 'jpeg', 'png', 'webp'] # autogenerated class to hold the input data class InputObservation: def __init__(self, image=None, latitude=None, longitude=None, author_email=None, date=None, time=None, date_option=None, time_option=None, uploaded_filename=None): self.image = image self.latitude = latitude self.longitude = longitude self.author_email = author_email self.date = date self.time = time self.date_option = date_option self.time_option = time_option self.uploaded_filename = uploaded_filename def __str__(self): return f"Observation: {self.image}, {self.latitude}, {self.longitude}, {self.author_email}, {self.date}, {self.time}, {self.date_option}, {self.time_option}, {self.uploaded_filename}" def __repr__(self): return f"Observation: {self.image}, {self.latitude}, {self.longitude}, {self.author_email}, {self.date}, {self.time}, {self.date_option}, {self.time_option}, {self.uploaded_filename}" def __eq__(self, other): return (self.image == other.image and self.latitude == other.latitude and self.longitude == other.longitude and self.author_email == other.author_email and self.date == other.date and self.time == other.time and self.date_option == other.date_option and self.time_option == other.time_option and self.uploaded_filename == other.uploaded_filename) def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash((self.image, self.latitude, self.longitude, self.author_email, self.date, self.time, self.date_option, self.time_option, self.uploaded_filename)) def to_dict(self): return { #"image": self.image, "image_filename": self.uploaded_filename.name if self.uploaded_filename else None, "image_md5": hashlib.md5(self.uploaded_filename.read()).hexdigest() if self.uploaded_filename else None, "latitude": self.latitude, "longitude": self.longitude, "author_email": self.author_email, "date": self.date, "time": self.time, "date_option": self.date_option, "time_option": self.time_option, "uploaded_filename": self.uploaded_filename } @classmethod def from_dict(cls, data): return cls(data["image"], data["latitude"], data["longitude"], data["author_email"], data["date"], data["time"], data["date_option"], data["time_option"], data["uploaded_filename"]) @classmethod def from_input(cls, input): return cls(input.image, input.latitude, input.longitude, input.author_email, input.date, input.time, input.date_option, input.time_option, input.uploaded_filename) @staticmethod def from_input(input): return InputObservation(input.image, input.latitude, input.longitude, input.author_email, input.date, input.time, input.date_option, input.time_option, input.uploaded_filename) @staticmethod def from_dict(data): return InputObservation(data["image"], data["latitude"], data["longitude"], data["author_email"], data["date"], data["time"], data["date_option"], data["time_option"], data["uploaded_filename"]) # define function to validate number, allowing signed float def is_valid_number(number:str) -> bool: pattern = r'^[-+]?[0-9]*\.?[0-9]+$' return re.match(pattern, number) is not None # Function to validate email address def is_valid_email(email): pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' return re.match(pattern, email) is not None # Function to extract date and time from image metadata def get_image_datetime(image_file): try: image = Image.open(image_file) exif_data = image._getexif() if exif_data is not None: for tag, value in exif_data.items(): if ExifTags.TAGS.get(tag) == 'DateTimeOriginal': return value except Exception as e: st.warning("Could not extract date from image metadata.") return None # an arbitrary set of defaults so testing is less painful... # ideally we add in some randomization to the defaults spoof_metadata = { "latitude": 23.5, "longitude": 44, "author_email": "super@whale.org", "date": None, "time": None, } #def display_whale(whale_classes:List[str], i:int, viewcontainer=None): def setup_input(viewcontainer: st.delta_generator.DeltaGenerator=None, _allowed_image_types: list=None, ): if viewcontainer is None: viewcontainer = st.sidebar if _allowed_image_types is None: _allowed_image_types = allowed_image_types viewcontainer.title("Input image and data") # 1. Image Selector uploaded_filename = viewcontainer.file_uploader("Upload an image", type=allowed_image_types) image_datetime = None # For storing date-time from image if uploaded_filename is not None: # Display the uploaded image image = Image.open(uploaded_filename) viewcontainer.image(image, caption='Uploaded Image.', use_column_width=True) # store the image in the session state st.session_state.image = image # Extract and display image date-time image_datetime = get_image_datetime(uploaded_filename) print(f"[D] image date extracted as {image_datetime}") m_logger.debug(f"image date extracted as {image_datetime} (from {uploaded_filename})") # 2. Latitude Entry Box latitude = viewcontainer.text_input("Latitude", spoof_metadata.get('latitude', "")) if latitude and not is_valid_number(latitude): viewcontainer.error("Please enter a valid latitude (numerical only).") m_logger.error(f"Invalid latitude entered: {latitude}.") # 3. Longitude Entry Box longitude = viewcontainer.text_input("Longitude", spoof_metadata.get('longitude', "")) if longitude and not is_valid_number(longitude): viewcontainer.error("Please enter a valid longitude (numerical only).") m_logger.error(f"Invalid latitude entered: {latitude}.") # 4. Author Box with Email Address Validator author_email = viewcontainer.text_input("Author Email", spoof_metadata.get('author_email', "")) if author_email and not is_valid_email(author_email): viewcontainer.error("Please enter a valid email address.") # 5. date/time ## first from image metadata if image_datetime is not None: time_value = datetime.datetime.strptime(image_datetime, '%Y:%m:%d %H:%M:%S').time() date_value = datetime.datetime.strptime(image_datetime, '%Y:%m:%d %H:%M:%S').date() else: time_value = datetime.datetime.now().time() # Default to current time date_value = datetime.datetime.now().date() ## if not, give user the option to enter manually date_option = st.sidebar.date_input("Date", value=date_value) time_option = st.sidebar.time_input("Time", time_value) observation = InputObservation(image=uploaded_filename, latitude=latitude, longitude=longitude, author_email=author_email, date=image_datetime, time=None, date_option=date_option, time_option=time_option) return observation