Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
process_automated_droplet.py 29.48 KiB
"""
MRCNN Particle Detection
Process images with MRCNN model trained on the droplet class.

The source code of "MRCNN Particle Detection" (https://git.rwth-aachen.de/avt-fvt/private/mrcnn-particle-detection) 
is based on the source code of "Mask R-CNN" (https://github.com/matterport/Mask_RCNN).

The source code of "Mask R-CNN" is licensed under the MIT License (MIT).
Copyright (c) 2017 Matterport, Inc.
Written by Waleed Abdulla

All source code modifications to the source code of "Mask R-CNN" in "MRCNN Particle Detection" 
are licensed under the Eclipse Public License v2.0 (EPL 2.0).
Copyright (c) 2022-2023 Fluid Process Engineering (AVT.FVT), RWTH Aachen University
Edited by Stepan Sibirtsev, Mathias Neufang & Jakob Seiler

The coyprights and license terms are given in LICENSE.

Ideas and a small code snippets were adapted from these sources:
https://github.com/mat02/Mask_RCNN
"""   

### ----------------------------------- ###
### Necessary Parameters and Data Names ###
### ----------------------------------- ###

# is the script executed on the cluster, e.g., RWTH High Performance Computing cluster? True = yes, False = no
cluster = False

### please specify only for non-cluster evaluations 

# generate detection masks? True = yes, False = no
masks = False
# is the program execution done on GPU or CPU? True = GPU, False = CPU
device = True
# input dataset path to find in "...\datasets\input\..."
dataset_path = r"test"              
# path to save the output images "...\datasets\output\..."
save_path = r"test"                 
# name of the excel results file to find in "...\datasets\output\..."
name_result_file = "test"         
# path of the MRCNN model to find in "...\models\..."
weights_path = r"test"
# MRCNN model name to find in "Mask_R_CNN\models\
weights_name = r"test"
# file format of images
file_format = "jpg"
# save n-th result image 
save_nth_image = 1  
# pixel size in [µm/px]. To read from Sopat log file enter pixelsize = 0
pixelsize = 1
# specify if you want the image to be center cropped before detection (x, y)
image_crop = None #(1931, 1521)

### specifications for the processing parameters

# number of images to process with on each GPU. 
# a 12GB GPU can typically handle 2 images of 1024x1024px.
# adjust based on your GPU memory and image sizes. 
# if only one GPU is used, this parameter is equivalent to batch size (BATCH_SIZE --> config.py).
images_gpu = 1
# max. image size
# select the value the MRCNN model was trained with.
image_max = 2048
# skip detections with confidence < value
confidence = 0.1

### specifications for filters

# detect reflections in droplets? Yes = True, No = False
detect_reflections = False
# detect/mark oval droplets? True = yes, False = no
detect_oval_droplets = True
# minimum aspect ratio: filter for elliptical shapes            
min_aspect_ratio = 0.9     
# detect adhesive droplets? 
detect_adhesive_droplets = False
# save coordinates of adhesive droplets detected
save_coordinates = False
# minimum velocity: threshold to filter adhesive droplets
# minimum distance [% of droplet mean diameter] that a droplet has to travel between 2 frames
min_velocity = 0.2
# minimum size difference: threshold to filter adhesive droplets
# [%] to be consindered a different droplet
min_size_diff = 0.4
# number of images that are being compared, this is necessary because adhesive droplets may not get detected every frame
n_images_compared = 3
# number of times a droplet has to be detected at a similar position to be defined as adhesive
n_adhesive_high = 3
n_adhesive_low = 2
low_distance_threshold = 0.05
# edge threshold: filter for image border intersecting droplets
edge_tolerance = 0.01

# use contrast adjustment? 0 = no, 1 = contrast limited adaptive histogramm equalization, 2 = contrast stretching  
contrast = 0

### ----------------------------------- ###
###             Initialization          ###
### ----------------------------------- ###

from PIL import Image
import os
import json
import sys
import random
import math
import re
import time
import glob
import itertools
import numpy as np
import tensorflow as tf
import matplotlib
import matplotlib.image as mpimg
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import cv2
import pandas as pd
from numpy import asarray
from random import random
from skimage import exposure
from pathlib import Path
matplotlib.use("agg")

start_time = time.time()

tf.to_float = lambda x: tf.cast(x, tf.float32)
# Root directory of the project
if cluster is False:
    ROOT_DIR = os.path.abspath("")
    WEIGHTS_DIR = os.path.join(ROOT_DIR, "models", weights_path, weights_name + '.h5')
    DATASET_DIR = os.path.join(ROOT_DIR, "datasets\\input", dataset_path)
    SAVE_DIR = os.path.join(ROOT_DIR, "datasets\\output", save_path)
    EXCEL_DIR = os.path.join(SAVE_DIR, name_result_file + '.xlsx')
    IMAGE_MAX = image_max
    MASKS = masks
    DEVICE = device
    IMAGES_GPU = images_gpu
    SAVE_NTH_IMAGE = save_nth_image
    DETECT_OVAL_DROPLETS = detect_oval_droplets
    DETECT_REFLECTIONS = detect_reflections
    MIN_ASPECT_RATIO = min_aspect_ratio
    PIXELSIZE = pixelsize
    DETECT_ADHESIVE_DROPLETS = detect_adhesive_droplets
    SAVE_COORDINATES = save_coordinates
    MIN_VELOCITY = min_velocity
    MIN_SIZE_DIFF = min_size_diff
    N_IMAGES_COMPARED = n_images_compared
    N_ADHESIVE_HIGH = n_adhesive_high
    N_ADHESIVE_LOW = n_adhesive_low
    LOW_DISTANCE_THRESHOLD = low_distance_threshold
    EDGE_TOLERANCE = edge_tolerance
    IMAGE_CROP = image_crop
    CONTRAST = contrast
    CONFIDENCE = confidence
    FILE_FORMAT = file_format
else:
    import argparse
    # Parse command line arguments
    parser = argparse.ArgumentParser(
        description='evaluation on cluster')
    parser.add_argument('--dataset_path', required=True,
                        help='Dataset path to find in Mask_R_CNN\datasets\input')
    parser.add_argument('--save_path', required=True,
                        help='Save path to find in Mask_R_CNN\datasets\output')
    parser.add_argument('--name_result_file', required=True,
                        help='Name of the excel result file to find in Mask_R_CNN\datasets\output')
    parser.add_argument('--weights_path', required=True,
                        help='Weights path to find in Mask_R_CNN\models')
    parser.add_argument('--weights_name', required=True,
                        help='Choose Neuronal Network / Epoch to find in Mask_R_CNN\models')
    parser.add_argument('--file_format', required=True,
                        help='')
    parser.add_argument('--masks', required=False, type=str,
                        default="False",
                        help='Generate detection masks?')
    parser.add_argument('--device', required=False, type=str,
                        default="True",
                        help='is the evaluation done on CPU or GPU? 1=GPU, 0=CPU')
    parser.add_argument('--detect_oval_droplets', required=True, type=str,
                        default="False",
                        help="")
    parser.add_argument('--detect_reflections', required=True, type=str,
                        default="False",
                        help="")                        
    parser.add_argument('--detect_adhesive_droplets', required=False, type=str,
                        default="False",
                        help="") 
    parser.add_argument('--save_coordinates', required=False, type=str,
                        default="False",
                        help="")                          
    parser.add_argument('--images_gpu', required=False, type=int,
                        default=1,
                        help='Number of images to train with on each GPU')
    parser.add_argument('--image_max', required=False, type=int,
                        default=1024,
                        help="max. image size")
    parser.add_argument('--save_nth_image', required=False, type=int,
                        default=1,
                        help="")
    parser.add_argument('--n_images_compared', required=False, type=int,
                        default=3,
                        help="")
    parser.add_argument('--n_adhesive_high', required=False, type=int,
                        default=3,
                        help="")                     
    parser.add_argument('--n_adhesive_low', required=False, type=int,
                        default=2,
                        help="")
    parser.add_argument('--image_crop', required=False, type=int,
                        default=None,
                        help="")
    parser.add_argument('--contrast', required=False, type=int,
                        default=0,
                        help="")
    parser.add_argument('--min_aspect_ratio', required=False, type=float,
                        default=0.9,
                        help="")
    parser.add_argument('--pixelsize', required=False, type=float,
                        default=1,
                        help="")
    parser.add_argument('--min_velocity', required=False, type=float,
                        default=0.2,
                        help="")
    parser.add_argument('--min_size_diff', required=False, type=float,
                        default=0.4,
                        help="")
    parser.add_argument('--low_distance_threshold', required=False, type=float,
                        default=0.05,
                        help="")
    parser.add_argument('--edge_tolerance', required=False, type=float,
                        default=0.01,
                        help="")
    parser.add_argument('--confidence', required=False, type=float,
                        default=0.5,
                        help="")                        

    args = parser.parse_args()
    ROOT_DIR = os.path.join("/rwthfs/rz/cluster", os.path.abspath("../.."))
    WEIGHTS_DIR = os.path.join(ROOT_DIR, "models", args.weights_path, args.weights_name + '.h5')
    DATASET_DIR = os.path.join(ROOT_DIR, "datasets/input", args.dataset_path)
    SAVE_DIR = os.path.join(ROOT_DIR, "datasets/output", args.save_path)
    EXCEL_DIR = os.path.join(SAVE_DIR, args.name_result_file + '.xlsx')
    FILE_FORMAT = args.file_format
    if args.detect_oval_droplets == "True":
        DETECT_OVAL_DROPLETS = True
    elif args.detect_oval_droplets == "False":
        DETECT_OVAL_DROPLETS = False
    if args.detect_reflections == "True":
        DETECT_REFLECTIONS = True
    elif args.detect_reflections == "False":
        DETECT_REFLECTIONS = False   
    if args.masks == "True":
        MASKS = True
    elif args.masks == "False":
        MASKS = False
    if args.device == "True":
        DEVICE = True
    elif args.device == "False":
        DEVICE = False
    if args.detect_adhesive_droplets == "True":
        DETECT_ADHESIVE_DROPLETS = True
    elif args.detect_adhesive_droplets == "False":
        DETECT_ADHESIVE_DROPLETS = False
    if args.save_coordinates == "True":
        SAVE_COORDINATES = True
    elif args.save_coordinates == "False":
        SAVE_COORDINATES = False        

    #
    IMAGE_MAX = args.image_max
    IMAGES_GPU = args.images_gpu
    SAVE_NTH_IMAGE = args.save_nth_image
    N_IMAGES_COMPARED = args.n_images_compared
    N_ADHESIVE_HIGH = args.n_adhesive_high
    N_ADHESIVE_LOW = args.n_adhesive_low
    IMAGE_CROP = args.image_crop
    CONTRAST = args.contrast   
    # 
    MIN_ASPECT_RATIO = args.min_aspect_ratio
    PIXELSIZE = args.pixelsize
    MIN_VELOCITY = args.min_velocity
    MIN_SIZE_DIFF = args.min_size_diff
    LOW_DISTANCE_THRESHOLD = args.low_distance_threshold
    EDGE_TOLERANCE = args.edge_tolerance
    CONFIDENCE = args.confidence

# Directory to save logs and trained model
MODEL_DIR = os.path.join(ROOT_DIR, "models")
Path(SAVE_DIR).mkdir(parents=True, exist_ok=True)

### mean pixel detectieren
images_mean_pixel = []
images_path = glob.glob(DATASET_DIR + "/*." + FILE_FORMAT)
for img_path in images_path:
    img = cv2.imread(img_path)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    if CONTRAST == 1:
        # adaptive Equalization
        img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        img = exposure.equalize_adapthist(img)
        img = img.astype('float32') * 255
    images_mean_pixel.append(img)
color_sum=[0,0,0]
for img2 in images_mean_pixel:
    pixels = asarray(img2)
    pixels = pixels.astype('float32')
    # calculate per-channel means and standard deviations
    means = pixels.mean(axis=(0, 1), dtype='float64')
    color_sum += means
mean_pixel = color_sum/len(images_mean_pixel)

# read pixelsize from JSON-File (if input data is from a Sopat measurement)
if PIXELSIZE == 0:
    sopat_find = [file for file in os.listdir(DATASET_DIR) if file.endswith('.json')]
    sopat_name = (DATASET_DIR + '/' + sopat_find[0])
    sopat_name_new = (DATASET_DIR + '/Sopat_Log.json')

    with open(sopat_name, "r",encoding="utf-8") as sopat_content:
        content_lines = sopat_content.readlines()

    current_line = 1
    with open(sopat_name_new, "w",encoding="utf-8") as sopat_content_new:
        for line in content_lines:
            if current_line == 30:
                pass
            else:
                sopat_content_new.write(line)
            current_line += 1
    sopat_data = json.load(open(sopat_name_new, "r", encoding="utf-8"))
    PIXELSIZE = sopat_data["sopatCamControlAcquisitionLog"]["conversionMicronsPerPx"]
      
# Import Mask RCNN
sys.path.append(ROOT_DIR)  # To find local version of the library
from mrcnn import utils
from mrcnn import visualize
from mrcnn.visualize import display_images
import mrcnn.model as modellib
from mrcnn.model import log
from mrcnn.config import Config

class DropletConfig(Config):
    """Configuration for training on the toy  dataset.
    Derives from the base Config class and overrides some values.
    """
    # Give the configuration a recognizable name
    NAME = "droplet"

    # NUMBER OF GPUs to use. When using only a CPU, this needs to be set to 1.
    GPU_COUNT = 1

    # Backbone network architecture
    # Supported values are: resnet50, resnet101.
    # You can also provide a callable that should have the signature
    # of model.resnet_graph. If you do so, you need to supply a callable
    # to COMPUTE_BACKBONE_SHAPE as well
    BACKBONE = "resnet50"

    # Generate detection masks
    #     False: Output only bounding boxes like in Faster-RCNN
    #     True: Generate masks as in Mask-RCNN
    if MASKS is True:
        GENERATE_MASKS = True
    else: 
        GENERATE_MASKS = False

    # We use a GPU with 12GB memory, which can fit two images.
    # Adjust down if you use a smaller GPU.
    if DEVICE is True:
        IMAGES_PER_GPU = IMAGES_GPU
    else:
        IMAGES_PER_GPU = 1

    # Number of classes (including background)
    NUM_CLASSES = 1 + 1  # Background + droplet

    # Skip detections with confidence < value
    DETECTION_MIN_CONFIDENCE = CONFIDENCE

    # Input image resizing
    IMAGE_MAX_DIM = IMAGE_MAX
    IMAGE_MIN_DIM = IMAGE_MAX_DIM

    MEAN_PIXEL = mean_pixel

### Configurations
config = DropletConfig()
config.display()

### Notebook Preferences

# Device to load the neural network on.
# Useful if you're training a model on the same 
# machine, in which case use CPU and leave the
# GPU for training.
if DEVICE is True:
    dev = "/gpu:0"  # /cpu:0 or /gpu:0
else:
    dev = "/cpu:0"  # /cpu:0 or /gpu:0

# Inspect the model in training or inference modes
# values: 'inference' or 'training'
# TODO: code for 'training' test mode not ready yet
TEST_MODE = "inference"

def get_ax(rows=1, cols=1, size=8):
    """Return a Matplotlib Axes array to be used in
    all visualizations in the notebook. Provide a
    central point to control graph sizes.
    
    Adjust the size attribute to control how big to render images
    """
    _, ax = plt.subplots(rows, cols, figsize=(size*cols, size*rows))
    return ax

### Load Model
# Create model in inference mode

with tf.device(dev):
    model = modellib.MaskRCNN(mode="inference", model_dir=MODEL_DIR, config=config)

# Load weights
    print("Loading weights ", WEIGHTS_DIR)
    model.load_weights(WEIGHTS_DIR, by_name=True)

### Run Detection
class Droplet:
    """Class to structure Droplet information in memory and calculate values for comparison
    """

    def __init__(self, roi, mask, img):
        self.roi = roi
        if config.GENERATE_MASKS:
            self.mask = mask
        # calculate edge lengths and center of roi. roi[1]-roi[3]=box_width. roi[0]-roi[2]=image_height
        self.range = (abs(roi[1] - roi[3]), abs(roi[0] - roi[2]))
        self.center = (abs((roi[0]+roi[2])//2), abs((roi[1]+roi[3])//2))
        # self.mean_diameter = abs((self.range[0]+self.range[1]+2)/2)
        self.mean_diameter = ((self.range[1]+1)*(self.range[0]+1)**2)**(1/3)
        self.mean_diameter_mm = round(self.mean_diameter * PIXELSIZE / 1000, 3)
        self.stuck = []
        self.check_roi()
        self.img = img

    def check_roi(self):
        """Run at Droplet creation to check for aspect ratio and whether it touches the edge
        """
        global DETECT_OVAL_DROPLETS, EDGE_TOLERANCE, MIN_ASPECT_RATIO, image_height, image_width
        if DETECT_OVAL_DROPLETS is True:
            if(
                # checks if bbox touches the edge
                self.roi[0] <= image_height*EDGE_TOLERANCE or self.roi[1] <= image_width*EDGE_TOLERANCE or 
                self.roi[2] >= image_height*(1-EDGE_TOLERANCE) or self.roi[3] >= image_width*(1-EDGE_TOLERANCE) 
            ):
                self.fault = 1
            else:
                self.fault = 0
        else:  
            if(
                # checks if bbox touches the edge
                self.roi[0] <= image_height*EDGE_TOLERANCE or self.roi[1] <= image_width*EDGE_TOLERANCE or 
                self.roi[2] >= image_height*(1-EDGE_TOLERANCE) or self.roi[3] >= image_width*(1-EDGE_TOLERANCE) or
                # checks if bbox is within allowed aspect ratio
                self.range[0] / self.range[1] >= 1 / MIN_ASPECT_RATIO or 
                self.range[0] / self.range[1] <= MIN_ASPECT_RATIO
            ):
                self.fault = 1
            else:
                self.fault = 0                
    # Parameter um klebende Tropfen zu erkennen
    def distance(self, center):
        """Returns distance to given coordinates
        """
        offset = np.array([center[0]-self.center[0], center[1]-self.center[1]])
        dist = np.linalg.norm(offset)
        dist /= self.mean_diameter
        return dist

    def size_difference(self, range):
        """Returns size difference in percent compared to given ranges
        """
        size_diff = abs(
            1-((self.range[0]*self.range[1]) / (range[0]*range[1])))
        return size_diff

def visualize_result(memory,counter_1):
    """collects all necessary parameters from the first entry in memory and then calls visualize.display_instances()
    Also appends droplet diameter to mean_diameter_total if no fault was determined
    """
    # Create Lists to pass onto display_instances()
    global stuck_droplet_data
    ax = get_ax(size=8)
    rois, masks, colors, diameter_vis_list = [], [], [], []
    xl = False
    if memory[0][0]:
        print(f"Visualizing {memory[0][0][0].img}")
    for droplet in memory[0][0]:
        rois.append(droplet.roi)
        diameter_vis_list.append(droplet.mean_diameter_mm)
        if config.GENERATE_MASKS:
            masks.append(droplet.mask)
        if droplet.fault == 0:
            mean_diameter_total.append(droplet.mean_diameter)
            colors.append((0, 1, 0))
        elif droplet.fault == 1:
            colors.append((1, 0, 0))
        elif droplet.fault == 4:
            colors.append((0, 0, 1))            
        elif DETECT_ADHESIVE_DROPLETS is False:
            mean_diameter_total.append(droplet.mean_diameter)
            colors.append((0, 1, 0))
        elif droplet.fault == 2:
            if len(droplet.stuck) >= N_ADHESIVE_HIGH:
                colors.append((1, 0.65, 0))
                if not xl:
                    stuck_droplet_data.append(
                        [droplet.img, droplet.mean_diameter_mm, "", droplet.stuck[0][0], droplet.stuck[0][1],
                        droplet.stuck[0][2], droplet.stuck[0][3], droplet.stuck[0][4]])
                    xl = True
                else:
                    stuck_droplet_data.append(
                        ["", droplet.mean_diameter_mm, "", droplet.stuck[0][0], droplet.stuck[0][1],
                         droplet.stuck[0][2], droplet.stuck[0][3], droplet.stuck[0][4]])
                
                for data in droplet.stuck[1:]:
                    stuck_droplet_data.append(
                        ["", "", "", data[0], data[1], data[2], data[3], data[4]])
            else:
                mean_diameter_total.append(droplet.mean_diameter)
                colors.append((0, 1, 0))
        elif droplet.fault == 3:
            if len(droplet.stuck) >= N_ADHESIVE_LOW:
                colors.append((1, 0.65, 0))
                if not xl: 
                    stuck_droplet_data.append(
                        [droplet.img, droplet.mean_diameter_mm, "<5%", droplet.stuck[0][0], droplet.stuck[0][1],
                        droplet.stuck[0][2], droplet.stuck[0][3], droplet.stuck[0][4]])
                    xl = True
                else:
                    stuck_droplet_data.append(
                        ["", droplet.mean_diameter_mm, "<5%", droplet.stuck[0][0], droplet.stuck[0][1],
                         droplet.stuck[0][2], droplet.stuck[0][3], droplet.stuck[0][4]])
                for data in droplet.stuck[1:]:
                    stuck_droplet_data.append(
                        ["", "", "<5%", data[0], data[1], data[2], data[3], data[4]])
            else:
                mean_diameter_total.append(droplet.mean_diameter)
                colors.append((0, 1, 0))
                
                
    # Convert Lists to numpy arrays and create placeholders for class_ids and scores
    rois = np.array(rois)
    masks = np.array(masks)
    class_ids = np.array(range(len(colors)))
    scores = np.array([1]*len(colors))
    img_name = "result_{}.jpg".format(os.path.splitext(memory[0][2])[0])
    if masks.any():
        masks = np.stack(masks, axis=-1)
    if config.GENERATE_MASKS:
        visualize.display_instances(memory[0][1], rois, masks, class_ids, scores, ax=ax, colors=colors, captions=diameter_vis_list,
                                    title=None, save_dir=SAVE_DIR, img_name=img_name, save_img=True, number_saved_images=SAVE_NTH_IMAGE, counter_1=counter_1)
    else:
        visualize.display_instances(memory[0][1], rois, None, class_ids, scores, ax=ax, show_mask=False, colors=colors, captions=diameter_vis_list,
                                    title=None, save_dir=SAVE_DIR, img_name=img_name, save_img=True, number_saved_images=SAVE_NTH_IMAGE, counter_1=counter_1)


def pre_processing(image, crop=None, size_y=1024, size_x=1024, contrast=0):
    """Crops image and optional contrast adjustments
    """
    if crop:
        size_x = crop[0]
        size_y = crop[1]
        image_height, image_width, _ = image.shape
        # center crop image to given size
        crop_y = (image_height-size_y)//2
        crop_x = (image_width-size_x)//2
        image = image[crop_y:crop_y+size_y, crop_x:crop_x+size_x]
        #im = Image.fromarray(image.astype(np.uint8))
        #im.save(os.path.join(SAVE_DIR, 'test.bmp'))
        # original = image.copy()
    
    if CONTRAST != 0:
        image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    if CONTRAST == 1:
        # adaptive Equalization
        image = exposure.equalize_adapthist(image)
        image = image.astype('float32') * 255
    elif CONTRAST == 2:
        # contrast stretching
        p2, p98 = np.percentile(image, (2, 98))
        image = exposure.rescale_intensity(image, in_range=(p2, p98))
    
    return image

# Tropfendetektion & Erkennung von klebenden Tropfen
memory = []
mean_diameter_total = []
measurements = []
stuck_droplet_data = []

counter_1 = SAVE_NTH_IMAGE
for filename in os.listdir(DATASET_DIR):
    if not filename.endswith('.json'):
        image = cv2.imread(os.path.join(DATASET_DIR, filename))
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        # pre process image
        if IMAGE_CROP or CONTRAST != 0:
            image = pre_processing(image, crop=IMAGE_CROP, contrast=CONTRAST)
        image_height, image_width, _ = image.shape
        image_length_max = max([image_width, image_height])
        results = model.detect([image], filename=filename, verbose=1)
        r = results[0]
        # Creates a List of Droplet Objects with detected information
        # Droplet object checks if droplet is too close to edge and whether aspec ratio is off on initialization
        droplets = []
        if config.GENERATE_MASKS:
            for i, roi in enumerate(r['rois']):
                droplets.append(Droplet(roi, r['masks'][:, :, i], filename))
        else:
            for roi in r['rois']:
                droplets.append(Droplet(roi, None, filename))
        print(f"Images in memory: {len(memory)}")

        if DETECT_REFLECTIONS == True:
            for a, b in itertools.combinations(droplets, 2):
                if(
                        a.center[0] > b.center[0]*0.95 and a.center[0] < b.center[0]*1.05 and
                        a.center[1] > b.center[1]*0.95 and a.center[1] < b.center[1]*1.05
                    ):      
                    if a.mean_diameter_mm < b.mean_diameter_mm:
                        a.fault = 4
                    else:
                        b.fault = 4       
        # If memory is not empty iterate through droplets in current picture
        if memory:
            for current in droplets:
                # If droplet has no faults so far compare to all droplets in memory
                if current.fault == 0:
                    shortest_distance = 2048
                    closest_size_diff = 0
                    # iterate memory from the back (most recent picture first) 
                    # t is a variable for time between reference image and current memory entry
                    for t, img in enumerate(reversed(memory), 1):
                        for droplet in img[0]:
                            measured_distance = current.distance(droplet.center)
                            measured_size_diff = current.size_difference(droplet.range)
                            measurements.append([measured_distance, measured_distance/t, measured_size_diff])
                            # Checks whether measured distance and size diff is within defined threshholds
                            if(measured_distance < MIN_VELOCITY * t and
                                measured_size_diff < MIN_SIZE_DIFF
                               ):
                                shortest_distance = measured_distance / t
                                closest_size_diff = measured_size_diff
                                current.stuck.append(((-1*t), droplet.mean_diameter_mm, measured_distance, measured_distance/t, measured_size_diff))
                                droplet.stuck.append((t, current.mean_diameter_mm, measured_distance, measured_distance/t,  measured_size_diff))
                                if measured_distance < LOW_DISTANCE_THRESHOLD:
                                    current.fault = 3
                                else:
                                    current.fault = 2
                                if droplet.fault == 2 or droplet.fault == 0:
                                    droplet.fault = current.fault
                                break
                            # Keeps track of shortest distances to help adjust threshholds
                            elif measured_distance < shortest_distance:
                                shortest_distance = measured_distance / t
                                closest_size_diff = measured_size_diff
                    if current.fault == 0:
                        print("Droplet is valid:")
                        print(f"\tshortest distance to any box:\t{round(shortest_distance,2)}")
                        print(f"\tsize difference to closest box:\t{round(closest_size_diff*100,2)}%")
                    else:
                        print("Droplet is stuck to Lens:")
                        print(f"\tdistance too close to box:\t{round(shortest_distance,2)}")
                        print(f"\tsize difference to that box:\t{round(closest_size_diff*100,2)}%")
                else:
                    print(
                        "Droplet is either to close to the edge or aspect ratio is off")
        # Depending on the number of images campared, visualizes the one furthest back and removes it from the list
        if(len(memory) == N_IMAGES_COMPARED):
            visualize_result(memory,counter_1)
            memory.pop(0)
        # Append List of current droplets and image information to the memory
        memory.append((droplets, image, filename))
        print("\n")
        if counter_1 == SAVE_NTH_IMAGE:
            counter_1 = 0
        counter_1 = counter_1 + 1

# Visualizes the remaining pictures
while memory:
    visualize_result(memory,counter_1)
    memory.pop(0)
    if counter_1 == SAVE_NTH_IMAGE:
        counter_1 = 0
    counter_1 = counter_1 + 1
### Translate Mean Diameter In Actual Droplet Sizes (mm)

# recalculate mean diameter
mean_diameter_total_resize = [(i * PIXELSIZE / 1000)
                              for i in mean_diameter_total]

### Convert Mean Diameter To Excel
df = pd.DataFrame(mean_diameter_total_resize).to_excel(
    EXCEL_DIR, header=False, index=False)
### Save measured data for threshold tuning
if SAVE_COORDINATES is True:
    pd.DataFrame(measurements).to_excel(
        os.path.join(SAVE_DIR, "xyz_measurements.xlsx"), header=False, index=False)
    pd.DataFrame(stuck_droplet_data).to_excel(
        os.path.join(SAVE_DIR, "stuck_droplet_data.xlsx"), header=False, index=False)

print("--- %s seconds ---" % (time.time() - start_time))