Coastline extraction using Landsat-8 multispectral imagery and band ratio technique



Due to anthropogenic activities and natural processes i.e. changes in sea level, sedimentation and wave energy coastlines are changing throughout the world. Coastline is contact line between sea and land, it is an important linear feature on earth's surface with dynamic nature.

Traditionally, the coastlines were manually digitized which was time-consuming and labour intensive. Remote sensing is a good alternative to extract coastlines using satellite imagery, this way both temporal and spatial aspects can be covered.

Satellite imagery of visible range can be used for interpretation and can be easily obtained. But the imageries covering infrared wavelength is best to extract boundary between land and water. So, the satellites which covers both visible and infrared spectrum are widely accepted for coastline extraction and mapping.

Landsat-8 multispectral imagery is used in the current study as it covers a wavelength ranging from 0.43 to 12.51 micrometers, and hence suitable for coastal and aerosol studies.

Neccessary Imports

import os
import glob
from zipfile import *

import arcgis
import arcpy
from import PolygonToLine
from datetime import datetime
import pandas as pd
from arcgis.features import GeoAccessor, GeoSeriesAccessor
from import convert_raster_to_feature
from arcgis.features.manage_data import overlay_layers
from arcgis.raster.functions import equal_to, greater_than, greater_than_equal, clip, apply, extract_band, stretch

Connect to your GIS

from arcgis import GIS
gis =  GIS('home')
gis2 = GIS(profile="your_enterprise_portal")

Get the data for analysis

Multispectral Landsat includes Landsat GLS and Landsat 8 imagery for use in visualization and analysis. This layer is time enabled and includes a number band combinations and indices rendered on demand. The Landsat 8 imagery includes eight multispectral bands from the Operational Land Imager (OLI) with 30m spatial resolution and two bands from the Thermal Infrared Sensor (TIRS) of 100m spatial resolution. It is updated daily with new imagery directly sourced from the Landsat on AWS collection.

landsat_item = gis.content.get('d9b466d6a9e647ce8d1dd5fe12eb434b')
landsat = landsat_item.layers[0]
Multispectral Landsat
Landsat multispectral and multitemporal imagery with on-the-fly renderings and indices for visualization and analysis. The Landsat 8 and 9 imagery in this layer is updated daily and is directly sourced from the USGS Landsat collection on AWS.Imagery Layer by esri
Last Modified: February 25, 2022
3 comments, 682,761 views

The buffer feature of 20 km was created from USA boundaries. This feature layer geometry will be used to get the Landsat-8 tiles of coastal areas.

aoi = gis.content.get('6ee0006608704d0389881e0bcf936778')
usa_coast_polygonFeature Layer Collection by api_data_owner
Last Modified: January 24, 2022
0 comments, 207 views

Prepare data for analysis

A map widget was created, which will define the extent of area of interest for the analysis.

m ='USA', 4)
m.extent = {'spatialReference': {'latestWkid': 3857, 'wkid': 102100},
 'xmin': -10124000.92647267,
 'ymin': 3341135.188124092,
 'xmax': -9823144.78314236,
 'ymax': 3463434.4333803146}

Multispectral imagery layers consist of data for whole world. First step, is to filter out the cloud free data for the study area which will be used in the analysis.

Create geometry of aoi

The aoi has four polygons representing: Islands, East coast, West coast and Alaska.

dfm = aoi.layers[0].query(out_fields="OBJECTID, type, code").sdf
01islands1{"rings": [[[-17328342.0256, 2126164.6003], [-...
12eastern_coast1{"rings": [[[-10796721.2644, 2984284.5892], [-...
23alaska1{"rings": [[[-17408919.7589, 11517244.7315], [...
34western_coast1{"rings": [[[-13019578.4789, 3857089.5857], [-...

In the current analysis the area of interest is situated at East coast. The geometry of aoi was created for filtering out the landsat-8 tiles for the study area. objectid=2 was used as it represents the east coast.

aoi_layer = aoi.layers[0]

## use the correct objectid by refering to the above dataframe for your area of interest. 
aoi_feature = aoi_layer.query(where="objectid=2")
aoi_geom = aoi_feature.features[0].geometry
aoi_geom['spatialReference'] = {'wkid':3857}

Filter out the Landsat-8 tiles

The landsat-8 tiles were filtered out on the basis of date of acquisition and cloud cover. The order of tiles is on the basis of cloud cover from low to high.

import pandas as pd
from datetime import datetime
selected = landsat.filter_by(where="(Category = 1) and (CloudCover<0.03)",
                             time=[datetime(2016, 1, 1), datetime(2019, 12, 31)],

df = selected.query(out_fields="AcquisitionDate, GroupName, CloudCover, DayOfYear", 
df['AcquisitionDate'] = pd.to_datetime(df['AcquisitionDate'], unit='ms')
019814002017-10-27 15:34:47LC08_L1GT_013034_20171027_20200902_02_T2_MTL0.000034{"rings": [[[-8124157.0645, 4595882.337700002]...
17084562019-08-30 15:34:41LC08_L1GT_013034_20190830_20200826_02_T2_MTL0.000034{"rings": [[[-8120029.161, 4595955.806000002],...
220145242016-09-30 16:26:09LC08_L1TP_021039_20160930_20200906_02_T1_MTL0.000039{"rings": [[[-9731664.0351, 3627576.194600001]...
320011652017-03-04 16:07:36LC08_L1TP_018040_20170304_20200905_02_T1_MTL0.000040{"rings": [[[-9261778.5607, 3442875.9834999964...
420216722016-04-05 16:38:19LC08_L1TP_023040_20160405_20200907_02_T1_MTL0.000040{"rings": [[[-10121801.5565, 3442899.711800001...
77120194402017-04-08 16:37:48LC08_L1TP_023039_20170408_20200904_02_T1_MTL0.029439{"rings": [[[-10078992.0945, 3627741.781099997...
77216269732018-08-22 16:57:07LC08_L1TP_026041_20180822_20200831_02_T1_MTL0.029541{"rings": [[[-10677865.1634, 3260354.100499999...
77329125252017-04-09 15:40:58LC08_L1TP_014036_20170409_20200904_02_T1_MTL0.029736{"rings": [[[-8393943.8222, 4199194.052900001]...
77419785792017-06-14 15:26:01LC08_L1TP_012029_20170614_20200903_02_T1_MTL0.029929{"rings": [[[-7672952.0537, 5657896.1329], [-7...
77519987122016-04-11 16:01:36LC08_L1TP_017041_20160411_20200907_02_T1_MTL0.029941{"rings": [[[-9132542.3423, 3260274.1598000005...

776 rows × 6 columns

## Create a list of tiles 
oid = df["OBJECTID"].tolist()

Create mosaic raster from tiles

After creating a list of landsat-8 tiles, next step was to create a mosaic raster for the study area. mosaic_by was used to lock all the tiles of landsat-8 list.

landsat.mosaic_by(method='lock_rasters', lock_rasters=oid)
landsat.extent = m.extent
<ImageryLayer url:"">

Preprocessing of the data

Extract bands from Landsat-8 tiles

From the mosaic raster, 3 single band raster were created for NIR, Red and Blue band using extract_band function.

Landsat-8 band spectrum table

Landsat-8 fifth band covers 0.85 - 0.88 micrometers of EMR spectrum. NIR band is best for delineating land and water interface. Due to high absorption of water in NIR spectrum the reflectance of water is around zero whereas vegetation and land has high reflectance value. In NIR imagery the water appears black and land & vegetation appears bright grey to white which makes it easier to delineate land and water boundary.

nir1 = extract_band(landsat, [5])
<ImageryLayer url:"">

Second band of landsat-8 represents blue spectrum and covers 0.45 - 0.51 micrometer. Water has high reflectance in blue spectrum.

blue1 = extract_band(landsat, [2])
<ImageryLayer url:"">

Fourth band represents red spectrum and covers 0.64 - 0.67 micrometer wavelength of EMR spectrum. Red spectrum is highly absorbed by vegetation and this is useful to delineate between soil and vegetation.

red1 = extract_band(landsat, [4])
<ImageryLayer url:"">

Band ratio technique

A recent remote sensing technique to extract coastline is Band Ratio. In this technique the DN values of bands are divided to create a binary raster. NIR, Red and Blue bands were used for creating the binary raster. NIR band is selected as it is able to delineate water-land boundary, Red band is important for vegetation and water content and Blue band has high reflectance in water bodies. In the current study the following band ratio formula was used:

                                 Blue>NIR & Blue>Red

The output of this formula is a binary raster which has 0 and 1 pixel value. 1 represents water (white pixels) and 0 represents land (black pixels) i.e. bare surface, vegetation etc.

binary1 = arcgis.raster.functions.raster_calculator([nir1, blue1, red1],  
                                                    ['nir', 'blue', 'red'],
<ImageryLayer url:"">

For vizualisation, stretch raster function was used to stretch function the histogram. After applying stretch the pixel values were changed to 0 and 255 where 0 represents land and 255 represents water.

dra1 = arcgis.raster.functions.stretch(binary1, 
<ImageryLayer url:"">

Postprocessing of results

Clip out extra area

The stretched binary raster was clipped out using aoi geometry to clip out the extra area and to minimize the processing time. Clip raster function was used.

clip_diff1 = clip(dra1, aoi_geom)
water_ras = greater_than([clip_diff1, 0], extent_type='FirstOf', cellsize_type='FirstOf')
water_raster ='water_ras'+str(, gis=gis2)
<ImageryLayer url:"">
dra2 = arcgis.raster.functions.stretch(water_ras, 
dra2_ras ="dra_raster"+str(, gis=gis2)
<ImageryLayer url:"">

Get the data in feature layer

The clipped binary raster was converted to polygon for coastline extraction. convert_raster_to_feature function was used for conversion.

b2_poly = convert_raster_to_feature(dra2_ras.layers[0], 

Coastline extraction

Filter out the water polygons

The feature layer was converted to dataframe using query and the polygons were ordered on the basis on Shape_Area column.

## Create dataframe from feature layer and get water polygons
dfm1 = b2_poly.layers[0].query('gridcode=255').sdf 

## Convert dataframe to feature layer
water_poly = gis2.content.import_data(dfm1, title='coast_poly_full'+str(
011255{"rings": [[[-10003378.34165575, 3471918.92970...
122255{"rings": [[[-9993388.342924597, 3471888.92970...
233255{"rings": [[[-9990868.343244672, 3471918.92970...
355255{"rings": [[[-9972718.345549935, 3471918.92970...
466255{"rings": [[[-9970468.345835714, 3471918.92970...
284065380053800255{"rings": [[[-9952228.348152416, 3367398.94297...
284075380153801255{"rings": [[[-9953518.34798857, 3367368.942979...
284085380253802255{"rings": [[[-9953668.347969517, 3367128.94300...
284095380353803255{"rings": [[[-9953818.347950466, 3367008.94302...
284105381853818255{"rings": [[[-10126408.326029453, 3401538.9386...

28411 rows × 4 columns

Extract the coastline polygon

The polygon with largest area represents the coastline.The polygon with highest value in Shape_Areacolumn was extracted by referring the df. The new dataframe was converted to feature layer using import_data function.

## Get the polygon with largest area as it will represent the coastline
df = water_poly.layers[0].query().sdf
df['MYAREA'] = df.SHAPE.geom.area
dfm5 = df[df['MYAREA']==df['MYAREA'].max()]
coast_poly = gis2.content.import_data(dfm5, title='coast_poly'+str(

Convert coastline polygons to line

The coastline polygon was converted to line using PolygonToLine arcpy function.

dc_df = pd.DataFrame.spatial.from_layer(coast_poly.layers[0])
0538182.271016e+0711.415751e+10255{"rings": [[[-10126444.9027, 3401549.207200005...
# Create empty dataframe
df = pd.DataFrame()

# Create polyline
df['SHAPE'] = dc_df.SHAPE.geom.boundary()
coast_polyline = df.spatial.to_featurelayer('coastline_polyline'+str(

Remove noise

A mask was created using the clipped raster, it represents the extent of raster. The mask will be used to remove the extra lines.

#Create a mask
mask = greater_than_equal([clip_diff1, -1], extent_type='FirstOf', cellsize_type='FirstOf')
mask_ras ='mask'+str(, gis=gis2)

# Create a mask to remove noise which covers whole extent of clipped raster
aoi2 = convert_raster_to_feature(mask_ras.layers[0], 

The coastline layer which we got in the previous step has some extra lines representing the boundary of aoi layer. To remove the extra lines a negative buffer of 10 meter was created using create_buffer function.

### Create neg_buffer to remove the noise
neg_buffer = arcgis.create_buffers(aoi2, 

Extra lines were masked out using the negative buffer boundary.

coastline_final = overlay_layers(coast_polyline.layers[0], neg_buffer.layers[0], output_name='extracted_coastline'+str(

Extracted Coastlines

m9='USA', 4)
## Create dataframe for visualization
dfm_1= coastline_final.layers[0].query(out_fields="*").sdf

## add the dataframe to map widget

Coastline for whole USA were extracted with the above workflow using Landsat-8 imagery. To see the results, click here


Band Ratio technique is a efficient method, which gives highly accurate results with less processing time and it is able to cover both temporal and spatial aspects of coastline changes. Band Ratio technique is a easy to calculate method which gives highly accurate results with less processing time. The workflow can be applied on any area using multispectral imagery i.e. Landsat-8, Sentinel-2, etc.

Literature resources

Research PaperShoreline Change Mapping Using Remote Sensing and GISAli Kourosh Niya, Ali Asghar Alesheikh, Mohsen Soltanpor, Mir Masoud Kheirkhahzarkesh
Research PaperShoreline change assessment using remote sensing and GIS techniques: a case study of the Medjerda delta coast, TunisiaMourad Louati & Hanen Saidi & Fouad Zargouni

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.