Using CREODIAS for oil spills monitoring

Author: Bartosz Staroń, a student of AGH University of Cracow, Faculty of Geology, Geophysics and Environmental Protectioncontact: staronbartosz02@gmail.com  

The project was carried out as part of a student internship at CloudFerro.

Oil spills are a common cause of pollution in seas and oceans, often resulting from ship malfunctions, defective pipelines, or port accidents. Due to their harmful effects on marine life and entire ecosystems, it is crucial to detect them as early as possible to minimize damage. 

Oil spills are visible in SAR radar images. Easy access to such data is provided by the CREODIAS platform. In this project, SAR images from the Sentinel-1 sensor were used. These images were acquired in Ground Range Detected (GRD) mode with Interferometric Wide swath acquisition and VV polarization. This means that the images were obtained by transmitting the signal vertically and receiving it vertically, and then they were projected onto the WGS 84 ellipsoid. For detecting oil spills, the VV polarization yields the best results due to its enhanced ability to detect changes on the water surface. 

While object detection algorithms can be effective for detecting oil spills in satellite images, in this project a U-Net convolutional neural network was chosen for semantic segmentation. This approach produces binary masks that predict where an oil spill may be present. Such masks are easy to interpret and intuitive even for those without specialized knowledge of the characteristics of oil spills on water. 

To train the U-Net network, a dataset is required, which can be prepared by manually labeling radar images provided through CREODIAS. In this project, the training dataset consisted of fifteen Sentinel-1 SAR images and their corresponding binary masks. These images were augmented to increase the dataset's size, and then both the images and masks were divided into smaller 1024x1024 pixel chunks to preserve the spatial context of oil spills. The final step in preparing the dataset involved selecting the fragments in which oil was detected (where the masks contained pixels with a value of 1) and an equal number of fragments without oil. This resulted in a 1:1 ratio of images with oil to images without oil, and the dataset comprised 3,360 smaller images, which were then split into training and test sets in a 70:30 ratio. 

Example of processed SAR image with binary mask. Contains Copernicus Sentinel-1 modified data (2021) 

The U-Net model used in the project featured a depth of four levels, which means it contained 9 convolutional blocks with the number of filters in each layer ranging from 32 to 512. The model was trained on the prepared dataset. For applying the model to new images, the input image must be split into 1024x1024 pixels fragments. Predictions are made for these patches, and then they are combined into a single binary mask that matches the dimensions of the input image.

Examples of processed SAR images with predicted binary masks. Contains Copernicus Sentinel-1 modified data (2021)  

Due to the size of the dataset, the model was trained using a GPU. CREODIAS provides access to virtual machines optimized for AI and machine learning models, including those suitable for training neural networks. Once your model is trained, or a pre-trained model is used, it can be applied to predict masks on new images accessed via CREODIAS.

Step-by-step code

Step 1: Import libraries

import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    import gc
    from keras.models import load_model

Step 2: Load SAR Images from CREODIAS

In this step, the file paths containing the Sentinel-1 SAR images are extracted by specifying a date range and the coordinates of a point that must be included in the image. 

#date in format YYYY-MM-DD 

start_date='2019-08-10' 

end_date='2019-09-30' 

#coordinates of POI in WGS84 

point_x='55'  

point_y='18' 

images=get_images(start_date, end_date, point_x, point_y) #getting images from eodata that intersect point within a defined time interval 

Step 3: Load model

Specify the path to the trained model and load it.

model_path='/path/to/model'
    model=load_model(model_path)

Step 4: Predict masks for images

In this step, mask predictions are made based on the original image, which has been divided into smaller fragments. Subsequently, the smaller masks are merged into a single mask that corresponds to the dimensions of the original image.

predicted_images=predictions(images, model, crop_patch, batch_size)

Step 5: Plotting images and predicted masks

After generating the masks, they can be displayed alongside the original images.

for pred_image, image in zip(predicted_images, images):
        plt.figure(figsize=(18, 9))
        plt.subplot(121)
        plt.imshow(image, cmap='gray')
        plt.subplot(122)
        plt.imshow(pred_image, cmap='gray')
        plt.show()

The functions utilized in the code above:

def make_patches(image, patch_size):
"""
Description:
This function creates patches from the input image
Parameters:
image - input image
patch_size - width and height of patch images
"""
height, width = image.shape[:2]
patches = []
for i in range(0, height, patch_size):
    for j in range(0, width, patch_size):
        patch = image[i:i + patch_size, j:j + patch_size]
        patches.append(patch)
return patches

def combine_patches(image, patches, patch_size):
"""
Description:
This function combines small images into an image of the original size
Parameters:
image - original image
patches - patch images array
patch_size - the size of the width and height of a single patch image
"""
height=image.shape[0]
width=image.shape[1]
image=np.zeros((height, width), dtype=patches[0].dtype)
patch_idx = 0
for i in range(0, height, patch_size):
    for j in range(0, width, patch_size):
        image[i:i + patch_size, j:j + patch_size] = patches[patch_idx]
        patch_idx += 1
return image

def predictions(images, model, crop_patch, batch_size):
"""
Description:
This function predicts masks for the input images
Parameters:
images - array of input images
model - U-net model
crop_patch - width and height of a patch images
batch_size - number of batch in a model
"""
predicted_images=[]
for image in images:
    image_patches=make_patches(image, crop_patch) #making patches from the image
    image_patches=np.array(image_patches)
    image_patches=np.expand_dims(image_patches, axis=-1) #adding another dimension
    img_predict=model.predict(image_patches, batch_size=batch_size) #predicting a mask for each image
    img_predict=np.squeeze(img_predict, axis=-1) #removal of the added dimension
    reconstructed=combine_patches(image, img_predict, crop_patch) #combinig patches into an image
    predicted_images.append(reconstructed)
    del image_patches, img_predict, reconstructed
    gc.collect()
return predicted_images

def get_images(start_date, end_date, point_x, point_y):
"""
Description:
This function gets images that intersect point within a defined time interval
Parameters:
start_date - date in format YYYY-MM-DD
end_date - date in format YYYY-MM-DD
point_x - X-coordinates of POI in WGS84
point_y - Y-coordinates of POI in WGS84
"""
images=[]
crop_patch=1024
k=0
n=0
kernel_size=25
sap_filtr=13
url=f'https://datahub.creodias.eu/odata/v1/Products?\$filter=((ContentDate/Start%20ge%20{start_date}T00:00:00.000Z%20and%20ContentDate/Start%20le%20{end_date}T23:59:59.999Z)%20and%20(Online%20eq%20true)%20and%20(OData.CSC.Intersects(Footprint=geography%27SRID=4326;POINT%20({point_x}%20{point_y})%27))%20and%20(((((Collection/Name%20eq%20%27SENTINEL-1%27)%20and%20(((Attributes/OData.CSC.StringAttribute/any(i0:i0/Name%20eq%20%27productType%27%20and%20i0/Value%20eq%20%27GRD%27)))))))))&\$expand=Attributes&\$expand=Assets&\$orderby=ContentDate/Start%20asc&\$top=20\'
products=json.loads(requests.get(url).text)
path_list=[]
for item in products[\'value\']:
path_list.append(item[\'S3Path\'])
del url
del products
for dir_path in path_list:
    matching_dir=glob.glob(os.path.join(dir_path, 'measurement'))
    for dir in matching_dir:
        for root, dirs, files in os.walk(dir):
            for file in files:
                file_path=None
                if file.endswith(\'001.tiff\'): #only first tiff file
                    file_path=glob.glob(os.path.join(dir, file))[0] #importing path
                if file_path:
                    '''operations on the original image'''
                    image=cv2.imread(file_path, cv2.IMREAD_UNCHANGED)
                    x_start, y_start=k*crop_patch, n*crop_patch #cooridnates of upper left corner
                    x_end, y_end=(image.shape[1]//crop_patch)*crop_patch, (image.shape[0]//crop_patch *crop_patch #coordinates of bottom right corner
                    image=image[y_start:y_end, x_start:x_end] #image cropping
                    del x_start, y_start, x_end, y_end
                    '''image normalization to grayscale (0-255) and operations on it'''
                    scaled=scale_array(image) #image scaling
                    scaled = cv2.normalize(scaled, None, 0, 255,
                    cv2.NORM_MINMAX).astype(np.uint8) #conversion from uint16 to uint8
                    del image
                    '''increase photo contrast'''
                    clahe = cv2.createCLAHE(clipLimit=4.0, tileGridSize=(kernel_size, kernel_size)) #histogram equalization
                    clahe_scaled = clahe.apply(scaled)
                    del scaled
                    '''denoising an image'''
                    median_image=cv2.medianBlur(clahe_scaled, sap_filtr) #salt and pepper noise filtering
                    del clahe_scaled
                    images.append(median_image)
return images