#PIoneers team - ESA Astro Pi Mission Space Lab 2020-2021 Phase 2
#Theme: Life on Earth

#---OUR CHOSEN TOPIC---:
#The aim of our project is to analyse terrestial(including forest fires) and aquatic vegetation(severe eutrophication, frequent algal blooms & more).
#Depending on the areas crossed by the ISS while our code is running, one of the 2 types of vegetation may have a higher degree of detail.

#---CODE DESCRIPTION---:

#1. We used Izzy’s NoIR camera with a blue optical filter for taking pictures at (2592x1944) resolution and also saved the current lat & long as EXIF data.
#   The sleep time after processing an image is 13 seconds.

#2. We alternated in between 6 different algorithms
#   (5 based on picture pixels analysis and one algorithm based exclusively on the position of the ISS compared to the Sun)
#   to decide whether to keep or delete the taken picture (we kept the pictures taken at daytime and deleted those at night which cannot be analysed).
#   By analysing every keep/delete algorithm accuracy & the time it takes, we will conclude which the best algorithm is
#   and further use that one when we participate again in the Astro Pi Mission Space Lab in the years to come.

#3. Our code will produce at most (178*60)/14=762 pictures (in the ideal case where all pictures are kept in short processing time [1 second]) with EXIF data,
#   as well as the logfile, and PIoneers_data.csv file, with the following header:
#   Date/time, Latitude, Longitude,
#   Pressure, Temperature, Humidity,
#   Picture brightness, Keep/delete algorithm, Keep/delete period, Picture verdict, Picture number.

import csv
import os
import math
import ephem
import cv2 as cv
from ephem import readtle, degree
from sense_hat import SenseHat
from datetime import datetime, timedelta
from time import sleep
from logzero import logger, logfile
from picamera import PiCamera

#The Python Imaging Library- Pillow (PIL) adds image processing capabilities to our Python main program.
#This library provides extensive file format support, an efficient internal representation, and fairly powerful image processing capabilities.
from PIL import Image #The Image module provides a class with the same name which is used to represent a PIL image.
#The Image module also provides a number of factory functions, including functions to load images from files, and to create new images.
from PIL import ImageStat #The ImageStat module calculates global statistics for an image, or for a region of an image.

#Recording the starting time (& also current time):
start_time=datetime.now()
current_time=datetime.now()

SIZE_MAX=2900000000 #bytes - maximum size of the files produced by our program, in order not to exceed the 3 GB memory limit
TIME_MAX=178 #minutes - maximum run time, in order not to exceed the 3-hour time limit
TRESHOLD_ALG1=40 #if the brightness is lower than this keeping treshold, the picture is deleted
TRESHOLD_ALG2=40
TRESHOLD_ALG3=40
TRESHOLD_ALG4=40
TRESHOLD_ALG5=40

#Getting the latest location of the ISS - TLE data:
name="ISS (ZARYA)"
line1="1 25544U 98067A   21044.55421846  .00000643  00000-0  19848-4 0  9993"
line2="2 25544  51.6436 234.2256 0002888   7.8795  97.0787 15.48961922269432"
iss=readtle(name, line1, line2) #Preparing to read the location of the ISS

#Saving the current path to main.py:
dir_path=os.path.dirname(os.path.realpath(__file__))

#Setting the logfile:
logfile(dir_path + "/PIoneers.log")

initial_size=0
#Initialising the picture counter:
picture_counter=1

#Setting restrictive custom twilight angle in degrees for day/night detection, which will be used for the 6th keep/delete picture algorithm:
sun=ephem.Sun()
twilight_deg=round(float(0))

#Creating a new csv file and defining its header:
def create_csv(data_file):
    with open(data_file, 'w') as f:
        writer=csv.writer(f)
        header=("Date/time",
                "Latitude","Longitude",
                "Pressure","Temperature","Humidity",
                "Picture brightness","Keep/delete algorithm",
                "Keep/delete period","Picture verdict","Picture number")
        writer.writerow(header)
        
#Adding a new line in an existing csv file:
def add_csv_data(data_file, data):
    with open(data_file, 'a') as f:
        writer=csv.writer(f)
        writer.writerow(data)

#Algorithm 1 - Converting the image to greyscale, and working with the average pixel brightness:
def alg1_keep_delete(file):
    try:
        initial_time=datetime.now()
        
        #When translating a color image to greyscale (mode 'L'), the library uses the ITU-R 601-2 luma transform
        #L = R * 299/1000 + G * 587/1000 + B * 114/1000:
        picture=Image.open(file).convert('L')
        stat=ImageStat.Stat(picture)
        brightness1=stat.mean[0] #mean=average (arithmetic mean) pixel level for each band in the image
    
        if brightness1<TRESHOLD_ALG1:
            keep_delete1="D"
        else:
            keep_delete1="K"
        
        final_time=datetime.now()
        delta1=final_time-initial_time
    
    #If an error occurs, the picture is deleted in the main program
    #and the brightness and delta are set on -1, respectively 0
    #(conventional error values):
    except Exception as e:
        logger.error(("alg1_keep_delete function error: {}: {}").format(e.__class__.__name__, e))
        brightness1=-1
        keep_delete1="D"
        delta1=start_time-start_time #workaround so as to return 0 in the required time format
        
    return brightness1, keep_delete1, delta1

#Algorithm 2 - Converting the image to greyscale, and working with the RMS pixel brightness:
def alg2_keep_delete(file):
    try:
        initial_time=datetime.now()
    
        picture=Image.open(file).convert('L')
        stat=ImageStat.Stat(picture)
        brightness2=stat.rms[0] #rms=root-mean-square for each band in the image
    
        if brightness2<TRESHOLD_ALG2:
            keep_delete2="D"
        else:
            keep_delete2="K"
        
        final_time=datetime.now()
        delta2=final_time-initial_time
        
    #If an error occurs, the picture is deleted in the main program
    #and the brightness and delta are set on -1, respectively 0
    #(conventional error values):
    except Exception as e:
        logger.error(("alg2_keep_delete function error: {}: {}").format(e.__class__.__name__, e))
        brightness2=-1
        keep_delete2="D"
        delta2=start_time-start_time #workaround so as to return 0 in the required time format
        
    return brightness2, keep_delete2, delta2

#Algorithm 3 - Based on average pixels, then transforming to "perceived brightness":
def alg3_keep_delete(file):
    try:
        initial_time=datetime.now()
    
        picture=Image.open(file)
        stat=ImageStat.Stat(picture)
        r, g, b=stat.mean
        brightness3=math.sqrt(0.241*(r**2) + 0.691*(g**2) + 0.068*(b**2))
    
        if brightness3<TRESHOLD_ALG3:
            keep_delete3="D"
        else:
            keep_delete3="K"
        
        final_time=datetime.now()
        delta3=final_time-initial_time
        
    #If an error occurs, the picture is deleted in the main program
    #and the brightness and delta are set on -1, respectively 0
    #(conventional error values):
    except Exception as e:
        logger.error(("alg3_keep_delete function error: {}: {}").format(e.__class__.__name__, e))
        brightness3=-1
        keep_delete3="D"
        delta3=start_time-start_time #workaround so as to return 0 in the required time format
        
    return brightness3, keep_delete3, delta3

#Algorithm 4 - Based on the RMS of pixels, then transforming to "perceived brightness":
def alg4_keep_delete(file):
    try:
        initial_time=datetime.now()
    
        picture=Image.open(file)
        stat=ImageStat.Stat(picture)
        r, g, b=stat.rms
        brightness4=math.sqrt(0.241*(r**2) + 0.691*(g**2) + 0.068*(b**2))
    
        if brightness4<TRESHOLD_ALG4:
            keep_delete4="D"
        else:
            keep_delete4="K"
        
        final_time=datetime.now()
        delta4=final_time-initial_time
        
    #If an error occurs, the picture is deleted in the main program
    #and the brightness and delta are set on -1, respectively 0
    #(conventional error values):
    except Exception as e:
        logger.error(("alg4_keep_delete function error: {}: {}").format(e.__class__.__name__, e))
        brightness4=-1
        keep_delete4="D"
        delta4=start_time-start_time #workaround so as to return 0 in the required time format
        
    return brightness4, keep_delete4, delta4

#Algorithm 5 - Reference algorithm based by analysing 1 in 4 pixels, inspired by 2020 Team Reforesting-Entrepreneurs:
def alg5_keep_delete(file, height, width):
    try:
        initial_time=datetime.now()
        file1=cv.imread(file)
        B_total=0
        R_total=0
        G_total=0
        
        #Totalling BGR values of every pixel:
        for line in range(0, height, 4):
            for column in range(0, width, 4):
                RGB=list(file1[line, column])
                B_total+=RGB[0]
                G_total+=RGB[1]
                R_total+=RGB[2]
        B_avg=B_total/((width*height)/16)
        G_avg=G_total/((width*height)/16)
        R_avg=R_total/((width*height)/16)

        #Translating the individual BGR values to a unified greyscale value:
        brightness5=(B_avg+G_avg+R_avg)/3
        if brightness5<TRESHOLD_ALG5:
            keep_delete5="D"
        else:
            keep_delete5="K"
        
        final_time=datetime.now()
        delta5=final_time-initial_time
        
    #If an error occurs, the picture is deleted in the main program
    #and the brightness and delta are set on -1, respectively 0
    #(conventional error values):
    except Exception as e:
        logger.error(("alg5_keep_delete function error: {}: {}").format(e.__class__.__name__, e))
        brightness5=-1
        keep_delete5="D"
        delta5=start_time-start_time #workaround so as to return 0 in the required time format
    
    return brightness5, keep_delete5, delta5

#Algorithm 6 - Based exclusively on the position of the ISS from the Sun (not on the picture):
def alg6_keep_delete():
    try:
        initial_time=datetime.now()
    
        iss.compute()
        observer=ephem.Observer() #setting a Sun observer with ISS coordinates and zero elevation
        observer.lat, observer.long, observer.elevation=iss.sublat, iss.sublong, 0
    
        sun.compute(observer)
        sun_angle_deg=float(round(math.degrees(sun.alt),6)) #Sun altitude - degrees only, rounded to 6 decimals
    
        if sun_angle_deg>twilight_deg:
            keep_delete6="K"
            brightness6=255 #covention so as to return a value for brightness (255=day/keep)
        else:
            keep_delete6="D"
            brightness6=0 #covention so as to return a value for brightness (0=night/delete)
        
        final_time=datetime.now()
        delta6=final_time-initial_time
    
    #If an error occurs, the picture is deleted in the main program
    #and the brightness and delta are set on -1, respectively 0
    #(conventional error values):
    except Exception as e:
        logger.error(("alg6_keep_delete function error: {}: {}").format(e.__class__.__name__, e))
        brightness6=-1
        keep_delete6="D"
        delta6=start_time-start_time #workaround so as to return 0 in the required time format
        
    return brightness6, keep_delete6, delta6

#Calculating the used memory space so as not to exceed 3 GB:
def used_memory_space(initial_path="."): #i_size=the initial size
    try:
        size=0
        for path, folders, files in os.walk(initial_path):
            for f in files:
                fp=os.path.join(path, f)
                size+=os.path.getsize(fp)
        return size

    except Exception as e:
        logger.error('{}: {})'.format(e.__class__.__name__, e))
        return initial_size+(picture_counter-1)*3500000

#Converting an ephem angle(degrees, minutes, seconds) to an EXIF-appropriate representation(rationals)
#For instance, '51:35:19.7' is converted to '51/1,35/1,197/10':
def convert(angle):
    degrees, minutes, seconds = (float(field) for field in str(angle).split(":"))
    exif_angle = f'{abs(degrees):.0f}/1,{minutes:.0f}/1,{seconds*10:.0f}/10'
    return degrees<0, exif_angle #the first returned value(boolean) is 0 if the angle is negative, and 1 otherwise

#Using the camera to capture a picture file with lat/long EXIF data:
def take_photo_lat_long(camera, image):
    #Getting the lat/long values from ephem:
    iss.compute()

    #Converting the latitude and longitude to EXIF-appropriate representations:
    south, exif_latitude = convert(iss.sublat)
    west, exif_longitude = convert(iss.sublong)

    #Setting the EXIF tags specifying the current location:
    camera.exif_tags['GPS.GPSLatitude'] = exif_latitude
    camera.exif_tags['GPS.GPSLatitudeRef'] = "S" if south else "N"
    camera.exif_tags['GPS.GPSLongitude'] = exif_longitude
    camera.exif_tags['GPS.GPSLongitudeRef'] = "W" if west else "E"

    #Capturing the image:
    camera.capture(image)
    
    #Returning the current latitude and longitude in degrees:
    return iss.sublat/degree, iss.sublong/degree

#Used memory before the program starts
#=>The space used by the program at a certain time will be the current size minus the initial size:
initial_size=used_memory_space(dir_path)

#Using the Sense HAT set of environmental sensors in order to monitor the surrounding conditions
#(pressure, temperature, and humidity) inside the Columbus Module:
sense=SenseHat()
create_csv("PIoneers_data.csv")

#Setting up the camera:
cam=PiCamera()
cam.resolution=(2592, 1944)

#Starting out with the first keep/delete algorithm:
used_algorithm=1

#Running a loop for approximately 3 hours
#and checking that the size of the produced data does not exceed 3 GB:
while(current_time<start_time+timedelta(minutes=TIME_MAX) and used_memory_space(dir_path)-initial_size<SIZE_MAX):
    try:
        #Taking a picture with EXIF lat & long
        #And recording the current lat & long from ephem library, in degrees,
        #coordinates which will further be added to the csv file:
        picture_file= dir_path + "/PIoneers_picture_" + str(picture_counter).zfill(4) + ".jpg"
        lat, long=take_photo_lat_long(cam, picture_file)
        
        if used_algorithm==1:
            brightness, keep_delete, delta=alg1_keep_delete(picture_file)
            
        elif used_algorithm==2:
            brightness, keep_delete, delta=alg2_keep_delete(picture_file)
            
        elif used_algorithm==3:
            brightness, keep_delete, delta=alg3_keep_delete(picture_file)
            
        elif used_algorithm==4:
            brightness, keep_delete, delta=alg4_keep_delete(picture_file)
            
        elif used_algorithm==5:
            brightness, keep_delete, delta=alg5_keep_delete(picture_file, 1944, 2592)
        
        elif used_algorithm==6:
            brightness, keep_delete, delta=alg6_keep_delete()
        
        if keep_delete=="D":
            picture_number=0 #the picture was deleted and does not exist
        else:
            picture_number=picture_counter
            
        #Adding the data to the csv file:
        row=(datetime.now(),
             lat, long,
             sense.pressure, sense.temperature, sense.humidity,
             brightness, used_algorithm,
             delta, keep_delete, picture_number
             )
        add_csv_data("PIoneers_data.csv", row)
        
        if keep_delete=="K":
            picture_counter=picture_counter+1 #keeping the picture, thus increasing the counter
        else:
            os.remove(picture_file) #deleting the picture
        
        used_algorithm=used_algorithm+1
        if used_algorithm==7:
            used_algorithm=1
        
        sleep(13)
        
        #Updating the current time:
        current_time=datetime.now()
        
    except Exception as e:
        logger.error('{}: {})'.format(e.__class__.__name__, e))