opticallyshallowdeep 1.1.3__tar.gz → 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {opticallyshallowdeep-1.1.3/opticallyshallowdeep.egg-info → opticallyshallowdeep-1.2.0}/PKG-INFO +33 -8
  2. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/README.md +31 -7
  3. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/opticallyshallowdeep/check_transpose.py +2 -2
  4. opticallyshallowdeep-1.2.0/opticallyshallowdeep/cloud_mask.py +103 -0
  5. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/opticallyshallowdeep/make_multiband_image.py +2 -2
  6. opticallyshallowdeep-1.2.0/opticallyshallowdeep/make_vertical_strips.py +34 -0
  7. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/opticallyshallowdeep/netcdf_to_multiband_geotiff.py +2 -2
  8. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/opticallyshallowdeep/process_as_strips.py +29 -32
  9. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/opticallyshallowdeep/run.py +32 -14
  10. opticallyshallowdeep-1.2.0/opticallyshallowdeep/write_georef_image.py +27 -0
  11. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0/opticallyshallowdeep.egg-info}/PKG-INFO +33 -8
  12. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/opticallyshallowdeep.egg-info/SOURCES.txt +2 -0
  13. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/opticallyshallowdeep.egg-info/requires.txt +1 -0
  14. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/setup.py +2 -2
  15. opticallyshallowdeep-1.1.3/opticallyshallowdeep/write_georef_image.py +0 -21
  16. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/LICENSE +0 -0
  17. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/MANIFEST.in +0 -0
  18. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/opticallyshallowdeep/__init__.py +0 -0
  19. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/opticallyshallowdeep/find_epsg.py +0 -0
  20. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/opticallyshallowdeep/models/SR.h5 +0 -0
  21. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/opticallyshallowdeep/models/TOA.h5 +0 -0
  22. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/opticallyshallowdeep/parse_string.py +0 -0
  23. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/opticallyshallowdeep.egg-info/dependency_links.txt +0 -0
  24. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/opticallyshallowdeep.egg-info/top_level.txt +0 -0
  25. {opticallyshallowdeep-1.1.3 → opticallyshallowdeep-1.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: opticallyshallowdeep
3
- Version: 1.1.3
3
+ Version: 1.2.0
4
4
  Summary: Identify optically shallow and deep waters in satellite imagery
5
5
  Author: Yulun Wu
6
6
  Author-email: yulunwu8@gmail.com
@@ -16,11 +16,12 @@ Requires-Dist: pyproj
16
16
  Requires-Dist: joblib
17
17
  Requires-Dist: scipy
18
18
  Requires-Dist: matplotlib
19
+ Requires-Dist: imagecodecs
19
20
  Requires-Dist: tensorflow
20
21
 
21
22
  # Optically-Shallow-Deep
22
23
 
23
- This python tool delineates optically shallow and deep waters in Sentinel-2 imagery. The tool uses a deep neural network that was trained on a diverse set of global images.
24
+ This python tool delineates optically shallow and deep waters in Sentinel-2 imagery. The tool uses a deep neural network (DNN) that was trained on a diverse set of global images.
24
25
 
25
26
  Supported input includes L1C SAFE files and ACOLITE-processed L2R netCDF files. The output geotiff contains probabilities of water pixels being optically shallow and deep.
26
27
 
@@ -77,24 +78,48 @@ pip3 install opticallyshallowdeep
77
78
 
78
79
  ## Quick Start
79
80
 
81
+ For L1C files:
82
+
80
83
  ```python
81
84
  import opticallyshallowdeep as osd
82
85
 
83
86
  # Input file
84
- file_in = 'test_folder_in/S2.SAFE' # or path to an ACOLTIE-generated L2R netCDF file
87
+ file_L1C = 'folder/S2.SAFE'
88
+
89
+ # Output folder
90
+ folder_out = 'folder/test_folder_out'
91
+
92
+ # Run the OSW/ODW classifier
93
+ osd.run(file_L1C, folder_out)
94
+ ```
95
+
96
+ For ACOLITE L2R files:
97
+
98
+ ```python
99
+ import opticallyshallowdeep as osd
100
+
101
+ # Input files
102
+ file_L1C = 'test_folder_in/S2.SAFE'
103
+ file_L2R = 'test_folder_in/L2R.nc'
85
104
 
86
105
  # Output folder
87
106
  folder_out = 'folder/test_folder_out'
88
107
 
89
108
  # Run the OSW/ODW classifier
90
- osd.run(file_in, folder_out)
109
+ osd.run(file_in, folder_out, file_L2R=file_L2R)
91
110
  ```
92
111
 
112
+ The L1C file is always required as it contains a cloud mask. Pixels within 8 pixels of the cloud mask are masked to reduce the impact of clouds.
93
113
 
94
- Output is a 3-band geotiff:
95
114
 
96
- - B1: Binary prediction (OSW/ODW)
97
- - B2: Prediction probability of OSW (100 means most likely OSW, 0 means most likely ODW)
98
- - B3: pixels that are masked out
115
+ Output is a 1-band geotiff, with values of prediction probability of OSW (100 means most likely OSW, 0 means most likely ODW). Non-water pixels are masked. It is recommended to use pixels between 0 and 40 as ODW, and pixels between 60 and 100 as OSW (publication in review).
99
116
 
100
117
  A log file, an intermediate multi-band geotiff, and a preview PNG are also generated in the output folder. They can be deleted after the processing.
118
+
119
+ ## Training, test, and validation data
120
+
121
+ All annotated shapefiles used in training, testing, and validating the DNN model are in the annotated_shapefiles folder, grouped by Sentinel-2 Scene ID.
122
+
123
+
124
+
125
+
@@ -1,6 +1,6 @@
1
1
  # Optically-Shallow-Deep
2
2
 
3
- This python tool delineates optically shallow and deep waters in Sentinel-2 imagery. The tool uses a deep neural network that was trained on a diverse set of global images.
3
+ This python tool delineates optically shallow and deep waters in Sentinel-2 imagery. The tool uses a deep neural network (DNN) that was trained on a diverse set of global images.
4
4
 
5
5
  Supported input includes L1C SAFE files and ACOLITE-processed L2R netCDF files. The output geotiff contains probabilities of water pixels being optically shallow and deep.
6
6
 
@@ -57,24 +57,48 @@ pip3 install opticallyshallowdeep
57
57
 
58
58
  ## Quick Start
59
59
 
60
+ For L1C files:
61
+
60
62
  ```python
61
63
  import opticallyshallowdeep as osd
62
64
 
63
65
  # Input file
64
- file_in = 'test_folder_in/S2.SAFE' # or path to an ACOLTIE-generated L2R netCDF file
66
+ file_L1C = 'folder/S2.SAFE'
67
+
68
+ # Output folder
69
+ folder_out = 'folder/test_folder_out'
70
+
71
+ # Run the OSW/ODW classifier
72
+ osd.run(file_L1C, folder_out)
73
+ ```
74
+
75
+ For ACOLITE L2R files:
76
+
77
+ ```python
78
+ import opticallyshallowdeep as osd
79
+
80
+ # Input files
81
+ file_L1C = 'test_folder_in/S2.SAFE'
82
+ file_L2R = 'test_folder_in/L2R.nc'
65
83
 
66
84
  # Output folder
67
85
  folder_out = 'folder/test_folder_out'
68
86
 
69
87
  # Run the OSW/ODW classifier
70
- osd.run(file_in, folder_out)
88
+ osd.run(file_in, folder_out, file_L2R=file_L2R)
71
89
  ```
72
90
 
91
+ The L1C file is always required as it contains a cloud mask. Pixels within 8 pixels of the cloud mask are masked to reduce the impact of clouds.
73
92
 
74
- Output is a 3-band geotiff:
75
93
 
76
- - B1: Binary prediction (OSW/ODW)
77
- - B2: Prediction probability of OSW (100 means most likely OSW, 0 means most likely ODW)
78
- - B3: pixels that are masked out
94
+ Output is a 1-band geotiff, with values of prediction probability of OSW (100 means most likely OSW, 0 means most likely ODW). Non-water pixels are masked. It is recommended to use pixels between 0 and 40 as ODW, and pixels between 60 and 100 as OSW (publication in review).
79
95
 
80
96
  A log file, an intermediate multi-band geotiff, and a preview PNG are also generated in the output folder. They can be deleted after the processing.
97
+
98
+ ## Training, test, and validation data
99
+
100
+ All annotated shapefiles used in training, testing, and validating the DNN model are in the annotated_shapefiles folder, grouped by Sentinel-2 Scene ID.
101
+
102
+
103
+
104
+
@@ -1,4 +1,6 @@
1
1
 
2
+ # Always output row, column, band
3
+
2
4
  def check_transpose(img):
3
5
  #if the #of bands is greater than the number of x or y cords
4
6
  y,x,b=img.shape
@@ -6,5 +8,3 @@ def check_transpose(img):
6
8
  img=img.transpose(1,2,0)
7
9
  return img
8
10
 
9
-
10
-
@@ -0,0 +1,103 @@
1
+
2
+
3
+
4
+
5
+ import os, sys
6
+ import rasterio
7
+ import numpy as np
8
+ from scipy import ndimage
9
+
10
+ def cloud_mask(file_L1C, buffer_size = 8):
11
+
12
+ print('Making cloud mask...')
13
+
14
+ files = os.listdir(file_L1C)
15
+ metadata = {}
16
+ metadata['file_L1C'] = file_L1C
17
+
18
+ # Identify paths
19
+ for i, fname in enumerate(files):
20
+ tmp = fname.split('.')
21
+ path = '{}/{}'.format(file_L1C,fname)
22
+
23
+ # Granules
24
+ if (fname == 'GRANULE'):
25
+ granules = os.listdir(path)
26
+
27
+ # Check if there is only one granule file
28
+ n_granule = 0
29
+
30
+ for granule in granules:
31
+ if granule[0]=='.':continue
32
+
33
+ n_granule += 1
34
+ if n_granule>1: sys.exit('Warning: more than 1 granule')
35
+
36
+ metadata['granule'] = '{}/{}/{}/IMG_DATA/'.format(file_L1C,fname,granule)
37
+ metadata['MGRS_tile'] = granule.split('_')[1][1:]
38
+ metadata['QI_DATA'] = '{}/{}/{}/QI_DATA'.format(file_L1C,fname,granule)
39
+
40
+
41
+ # # MGRS
42
+ # tile = metadata['MGRS_tile'] + '54905490'
43
+ # d = m.toLatLon(tile)
44
+ # metadata['lat'] = d[0]
45
+ # metadata['lon'] = d[1]
46
+
47
+ # Band files
48
+ image_files = os.listdir(metadata['granule'])
49
+ for image in image_files:
50
+ if image[0]=='.':continue
51
+ if image[-4:]=='.xml':continue
52
+ tmp = image.split('_')
53
+ metadata[tmp[-1][0:3]] = '{}/{}/{}/IMG_DATA/{}'.format(file_L1C,fname,granule,image)
54
+
55
+ ### Load built-in mask
56
+
57
+ gml_file = "{}/MSK_CLOUDS_B00.gml".format(metadata['QI_DATA'])
58
+ jp2_file = "{}/MSK_CLASSI_B00.jp2".format(metadata['QI_DATA'])
59
+
60
+ # For imagery before processing baseline 4: Jan 25, 2022
61
+ if os.path.exists(gml_file):
62
+
63
+ # Built-in cloud mask
64
+ import geopandas as gpd
65
+ from rasterio.features import geometry_mask
66
+
67
+ # Load a raster as the base of the mask
68
+ image = metadata['B02']
69
+
70
+ with rasterio.open(image) as src:
71
+ # Read the raster data and transform
72
+ raster_data = src.read(1)
73
+ transform = src.transform
74
+ crs = src.crs
75
+
76
+ try:
77
+ # Read GML file
78
+ gdf = gpd.read_file(gml_file)
79
+
80
+ # Create a mask using the GML polygons and the GeoTIFF metadata
81
+ mask_cloud = geometry_mask(gdf['geometry'], transform=transform, out_shape=raster_data.shape, invert=True)
82
+
83
+ # Sometimes the GML file contains no information, assume no clouds in such case
84
+ except:
85
+ mask_cloud = np.zeros_like(raster_data)
86
+
87
+ # For imagery processing baseline 4
88
+ elif os.path.exists(jp2_file):
89
+ band_ds = rasterio.open(jp2_file)
90
+ band_array = band_ds.read(1)
91
+ mask_cloud = band_array == 1
92
+ mask_cloud = np.repeat(np.repeat(mask_cloud, 6, axis=0), 6, axis=1)
93
+
94
+ else:
95
+ sys.exit('Warning: cloud mask missing in {}.'.format(metadata['QI_DATA']))
96
+
97
+ # To buffer: https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.binary_dilation.html
98
+ struct1 = ndimage.generate_binary_structure(2, 1)
99
+ mask_cloud_buffered = ndimage.binary_dilation(mask_cloud, structure=struct1,iterations=buffer_size).astype(mask_cloud.dtype)
100
+
101
+ print('Done')
102
+ return mask_cloud_buffered
103
+
@@ -13,7 +13,7 @@ def make_multiband_image(file_in,folder_out):
13
13
  imageFile = os.path.join(folder_out,basename) + '.tif'
14
14
 
15
15
  if os.path.exists(imageFile):
16
- print('Geotiff exists: ' + str(imageFile))
16
+ print('Multi-band geotiff exists: ' + str(imageFile))
17
17
  else:
18
18
  print('Making multi-band geotiff: ' + str(imageFile))
19
19
 
@@ -26,7 +26,7 @@ def make_multiband_image(file_in,folder_out):
26
26
  res = int(band2.transform[0])
27
27
  arrayList = []
28
28
  for bandFile in S2Files:
29
- print("Reading band: {}".format(bandFile))
29
+ # print("Reading band: {}".format(bandFile))
30
30
  band = rasterio.open(bandFile)
31
31
  ar = band.read(1)
32
32
  bandRes = int(band.transform[0])
@@ -0,0 +1,34 @@
1
+
2
+
3
+ import numpy as np
4
+
5
+
6
+ def make_vertical_strips(full_img):
7
+ '''use to save ram, process bigger images faster, and it overlaps so middle image is not
8
+ distorted from how edge pixels are handled'''
9
+
10
+ # number of dimensions
11
+ n_dim = full_img.ndim
12
+
13
+ if n_dim == 2:
14
+ height, width = full_img.shape #this is done so strips do not have artifacts from kernals
15
+ overlap_size = 16 #size of overlap, max tile size is 15, so there is a 1px buffer
16
+ strip1 = full_img[:, :width//5 + overlap_size]#left overlap
17
+ strip2 = full_img[:, width//5: 2*width//5+ overlap_size]# left half overlap
18
+ strip3 = full_img[:, 2*width//5:3*width//5 + overlap_size]#left overlap
19
+ strip4 = full_img[:, 3*width//5:4*width//5+ overlap_size]#left overlap
20
+ strip5 = full_img[:, 4*width//5:width]# no overlap
21
+ elif n_dim == 3:
22
+ height, width, _ = full_img.shape #this is done so strips do not have artifacts from kernals
23
+ overlap_size = 16 #size of overlap, max tile size is 15, so there is a 1px buffer
24
+ strip1 = full_img[:, :width//5 + overlap_size, :]#left overlap
25
+ strip2 = full_img[:, width//5: 2*width//5+ overlap_size, :]# left half overlap
26
+ strip3 = full_img[:, 2*width//5:3*width//5 + overlap_size, :]#left overlap
27
+ strip4 = full_img[:, 3*width//5:4*width//5+ overlap_size, :]#left overlap
28
+ strip5 = full_img[:, 4*width//5:width, :]# no overlap
29
+ else:
30
+ import sys
31
+ sys.exit('Unknown dimension(s) of input imagery to be splited into strips')
32
+
33
+ return [strip1,strip2,strip3,strip4,strip5]
34
+
@@ -18,7 +18,7 @@ def netcdf_to_multiband_geotiff(netcdf_file, folder_out):
18
18
  output_geotiff_file = os.path.join(folder_out, tif_base)
19
19
 
20
20
  if os.path.exists(output_geotiff_file):
21
- print('Geotiff exists: ' + str(output_geotiff_file))
21
+ print('Multi-band geotiff exists: ' + str(output_geotiff_file))
22
22
 
23
23
  else:
24
24
 
@@ -44,7 +44,7 @@ def netcdf_to_multiband_geotiff(netcdf_file, folder_out):
44
44
  for i, band_name in enumerate(band_names):
45
45
  ar = nc.variables[band_name][:,:] * 10_000
46
46
  ar[np.isnan(ar)] = value_for_nodata
47
- data_array[i] = ar
47
+ data_array[i] = ar.astype('int16')
48
48
 
49
49
  lat = nc.variables['lat'][:,:]
50
50
  lon = nc.variables['lon'][:,:]
@@ -16,41 +16,32 @@ from scipy.ndimage import binary_dilation
16
16
  import tensorflow as tf
17
17
  from tensorflow.keras.layers import Input, Dense
18
18
  from tensorflow.keras.models import Model,load_model
19
+ tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)
19
20
 
20
- def process_as_strips (full_img, image_path, if_SR, model_path, selected_columns, model_columns, file_in):
21
- strip1, strip2, strip3, strip4,strip5=make_vertical_strips(full_img) #create strips with overlap
22
- striplist=[strip1, strip2, strip3, strip4,strip5] #make strip list
21
+ from .make_vertical_strips import make_vertical_strips
22
+
23
+
24
+ def process_as_strips (full_img, image_path, if_SR, model_path, selected_columns, model_columns, file_in, cloud_list):
25
+ striplist=make_vertical_strips(full_img) #create a list of strips with overlap
23
26
  RGBlist=[]
27
+
24
28
  for n in range(len(striplist)):
25
- print(" Strip {}/5".format(n+1))
26
- strip_p=process_img_to_rgb(striplist[n],image_path, if_SR, model_path, selected_columns, model_columns, file_in) #output is RGB of image
29
+ print("Strip {}/5".format(n+1))
30
+ strip_p=process_img_to_rgb(striplist[n],image_path, if_SR, model_path, selected_columns, model_columns, file_in, cloud_list[n]) #output is RGB of image
27
31
  RGBlist.append(strip_p) #append processed strip to RGB list
28
32
  RGB_img=join_vertical_strips(RGBlist[0], RGBlist[1], RGBlist[2], RGBlist[3],RGBlist[4])
29
33
  plot_RGB_img(RGB_img, image_path) #save the final image
30
34
  return RGB_img
31
35
 
32
-
33
- def make_vertical_strips(full_img):
34
- '''use to save ram, process bigger images faster, and it overlaps so middle image is not
35
- distorted from how edge pixels are handled'''
36
- height, width, _ = full_img.shape #this is done so strips do not have artifacts from kernals
37
- overlap_size = 16 #size of overlap, max tile size is 15, so there is a 1px buffer
38
- strip1 = full_img[:, :width//5 + overlap_size, :]#left overlap
39
- strip2 = full_img[:, width//5: 2*width//5+ overlap_size, :]# left half overlap
40
- strip3 = full_img[:, 2*width//5:3*width//5 + overlap_size, :]#left overlap
41
- strip4 = full_img[:, 3*width//5:4*width//5+ overlap_size, :]#left overlap
42
- strip5 = full_img[:, 4*width//5:width, :]# no overlap
43
- return strip1,strip2,strip3,strip4,strip5
44
-
45
- def process_img_to_rgb(img,file_path, if_SR, model_path, selected_columns, model_columns, file_in):
36
+ def process_img_to_rgb(img, file_path, if_SR, model_path, selected_columns, model_columns, file_in, img_cloud):
46
37
  img,img_name,correction=correct_baseline(img,file_path, if_SR, file_in)#used on slices or whole images
47
- final_cord=get_water_pix_coord(img,correction, if_SR) #getting coordinates of water pixels
38
+ final_cord=get_water_pix_coord(img,correction, if_SR, img_cloud) #getting coordinates of water pixels
48
39
  if len(final_cord)==0:
49
40
  RGB_img=make_blank_img(img)
50
41
  return RGB_img
51
42
  else:
52
43
  # print(" {} {} Coordinates of non-glinty water pixels".format(time_tracker(start_time),len(final_cord)))
53
- print(" Processing {} unmasked water pixels".format(len(final_cord)))
44
+ print(" Processing {} water pixels".format(len(final_cord)))
54
45
 
55
46
  filter_image = process_image_with_filters(img, selected_columns) #creating a filter image to extract values from
56
47
  edge_nodata_list = select_edge_and_buffer_no_data_pixels (img,correction, if_SR) #selecting pixels for slow processing
@@ -60,7 +51,7 @@ def process_img_to_rgb(img,file_path, if_SR, model_path, selected_columns, model
60
51
  cord_list, pred_results, con_1=load_model_and_predict_pixels(value_list,model_path,cord_list, if_SR)
61
52
  RGB_img=make_output_images_fast(cord_list, pred_results, con_1,img)#make RBG image
62
53
  # print(" {} Finished model predictions".format(time_tracker(start_time)))
63
- print(" Strip complete")
54
+ print(" Complete")
64
55
 
65
56
  del cord_list, pred_results, con_1,img
66
57
  gc.collect()
@@ -88,12 +79,10 @@ def correct_baseline(img,file_path, if_SR, file_in):
88
79
  xml = minidom.parse(xml_path)#look at xml for correction first
89
80
  tdom = xml.getElementsByTagName('RADIO_ADD_OFFSET')#if this tag exists it is after baseline 4
90
81
 
91
-
92
82
  tdom_URI = xml.getElementsByTagName('PRODUCT_URI')
93
83
  S2_URI = tdom_URI[0].firstChild.nodeValue
94
84
  img_name = S2_URI[39:44]
95
85
 
96
-
97
86
  # If no RADIO_ADD_OFFSET
98
87
  if len(tdom) == 0:
99
88
 
@@ -108,7 +97,7 @@ def correct_baseline(img,file_path, if_SR, file_in):
108
97
  '''Correction is a very important variable, since in some of the images we need to add 1000 in order to
109
98
  correct for baseline 4. In these instances, 0 becomes 1000. There are times where we need to mask out 0 pixels
110
99
  or avoid 0, so we use correction as a variable for pixels that are originally 0'''
111
- print(' Adjusted pixel value for before Baseline 4 processing')
100
+ # print(' Adjusted pixel value for before Baseline 4 processing')
112
101
  del chunks
113
102
 
114
103
  # If there is RADIO_ADD_OFFSET
@@ -119,7 +108,7 @@ def correct_baseline(img,file_path, if_SR, file_in):
119
108
  del img
120
109
  return imgf, img_name, correction
121
110
 
122
- def get_water_pix_coord(img,correction, if_SR):
111
+ def get_water_pix_coord(img,correction, if_SR, img_cloud):
123
112
  #creates the mask of what is water by using Glint threshold, NDWI, NDSI...
124
113
  if if_SR == False:
125
114
  glint_t= 1500#this glint thresholds were used when training the model.
@@ -129,6 +118,7 @@ def get_water_pix_coord(img,correction, if_SR):
129
118
  glr, glc = glint_coordinates
130
119
  glint_coordinates_list = list(zip(glr, glc))#where not glint
131
120
  del glr, glc,glint_coordinates
121
+
132
122
  b3,b8,b11 = img[:, :, 2].astype(np.float32),img[:, :, 7].astype(np.float32),img[:, :, 9].astype(np.float32)
133
123
  NDWI = (b3 - b8) / (b3 + b8 +1e-8) #NDWI with avoiding div 0
134
124
  coordinates_NDWI = np.where(NDWI > 0)#where water (used to be 0)
@@ -136,6 +126,7 @@ def get_water_pix_coord(img,correction, if_SR):
136
126
  coordinate_list_NDWI = list(zip(ndwir,ndwic))
137
127
  del b8,NDWI, coordinates_NDWI,ndwir, ndwic
138
128
  gc.collect()
129
+
139
130
  NDSI = (b3 - b11) / (b3 + b11 +1e-8) #NDSI with avoiding div 0
140
131
  coordinates_NDSI = np.where(NDSI < .42)#where not snow
141
132
  ndsir, ndsic = coordinates_NDSI
@@ -143,11 +134,17 @@ def get_water_pix_coord(img,correction, if_SR):
143
134
  del b3,b11,NDSI,coordinates_NDSI,ndsir,ndsic
144
135
  gc.collect()
145
136
 
137
+ coordinates_cloud = np.where(np.invert(img_cloud))
138
+ cloudr, cloudc = coordinates_cloud
139
+ coordinate_list_cloud = list(zip(cloudr, cloudc))#where not glint
140
+ del cloudr, cloudc,coordinates_cloud
141
+ gc.collect()
142
+
146
143
  # L1C
147
144
  if if_SR == False:
148
145
  ND_coordinates = np.column_stack(np.where(np.all((img > correction) & (img < 30000), axis=-1)))#where not no data (in any band)
149
146
  ND_coordinates_list = list(map(tuple, ND_coordinates))
150
- common_coordinates_set = set(glint_coordinates_list) & set(ND_coordinates_list)& set(coordinate_list_NDWI)& set(coordinate_list_NDSI)
147
+ common_coordinates_set = set(glint_coordinates_list) & set(ND_coordinates_list)& set(coordinate_list_NDWI)& set(coordinate_list_NDSI)& set(coordinate_list_cloud)
151
148
  common_coordinates_list = list(common_coordinates_set) # Convert set to list
152
149
  del ND_coordinates,ND_coordinates_list,common_coordinates_set,glint_coordinates_list
153
150
 
@@ -157,7 +154,7 @@ def get_water_pix_coord(img,correction, if_SR):
157
154
  ND_coordinates_list = list(map(tuple, ND_coordinates))
158
155
  Acolite_pix=np.column_stack(np.where(np.all((img <= 3000), axis=-1)))#threshold from ACOLITE
159
156
  Acolite_pix_list = list(map(tuple, Acolite_pix))
160
- common_coordinates_set = set(glint_coordinates_list)&set(Acolite_pix_list)&set(ND_coordinates_list)&set(coordinate_list_NDWI)
157
+ common_coordinates_set = set(glint_coordinates_list)&set(Acolite_pix_list)&set(ND_coordinates_list)&set(coordinate_list_NDWI)& set(coordinate_list_cloud)
161
158
  common_coordinates_list = list(common_coordinates_set)
162
159
  del common_coordinates_set,Acolite_pix,Acolite_pix_list,ND_coordinates,ND_coordinates_list,glint_coordinates_list
163
160
  gc.collect()
@@ -168,7 +165,7 @@ def make_blank_img(img):
168
165
  Y_b, X_b, b = img.shape #sometimes the image is all no data or the correction value, in this instance, we make a blank image
169
166
  RGB_img = np.zeros((Y_b, X_b, 3), dtype=np.uint8)
170
167
  # print(' {} Blank strip added. No valid water pixels'.format(time_tracker(start_time)))
171
- print(' {} Blank strip added. No valid water pixels')
168
+ print(' No valid water pixels')
172
169
  return RGB_img
173
170
 
174
171
  def time_tracker(start_time):
@@ -415,11 +412,11 @@ def plot_RGB_img(RGB_img, image_path):
415
412
  import matplotlib.pyplot as plt
416
413
  fig, ax = plt.subplots(1, 3, figsize=(10, 10), sharex=True, sharey=True)
417
414
  ax[0].imshow(RGB_img[:,:,0])#plotting to see what OSW/ODW looks like
418
- ax[0].set_title('Prediction Image')
415
+ ax[0].set_title('Prediction based on 0.5 threshold')
419
416
  ax[1].imshow(RGB_img[:,:,1])
420
- ax[1].set_title('Prediction Probability Image')
417
+ ax[1].set_title('Prediction probability')
421
418
  ax[2].imshow(RGB_img[:,:,2])
422
- ax[2].set_title('Masked Image')
419
+ ax[2].set_title('Non-water mask')
423
420
  # plt.show()
424
421
 
425
422
  out_path = image_path.replace('.tif','.png')
@@ -11,13 +11,17 @@ from .parse_string import parse_string
11
11
 
12
12
  from .write_georef_image import write_georef_image
13
13
  from .netcdf_to_multiband_geotiff import netcdf_to_multiband_geotiff
14
+ from .make_vertical_strips import make_vertical_strips
15
+ from .cloud_mask import cloud_mask
14
16
 
15
17
 
16
- def run(file_in,folder_out, to_log=True):
18
+
19
+
20
+ def run(file_L1C, folder_out, file_L2R = None, to_log=True):
17
21
 
18
22
  ### Check the two
19
- if not os.path.exists(file_in):
20
- sys.exit('file_in does not exist: ' + str(file_in))
23
+ if not os.path.exists(file_L1C):
24
+ sys.exit('file_L1C does not exist: ' + str(file_L1C))
21
25
 
22
26
  # folder_out: if not exist -> create it
23
27
  if not os.path.exists(folder_out):
@@ -28,7 +32,7 @@ def run(file_in,folder_out, to_log=True):
28
32
  # Start logging in txt file
29
33
  orig_stdout = sys.stdout
30
34
 
31
- log_base = os.path.basename(file_in).replace('.nc','.txt').replace('.safe','.txt').replace('.SAFE','.txt')
35
+ log_base = os.path.basename(file_L1C).replace('.safe','.txt').replace('.SAFE','.txt')
32
36
  log_base = 'OSD_log_'+ log_base
33
37
  log_file = os.path.join(folder_out,log_base)
34
38
 
@@ -47,10 +51,11 @@ def run(file_in,folder_out, to_log=True):
47
51
  sys.stdout = Logger(log_file)
48
52
 
49
53
  # Metadata
50
- print('=== ENVIRONMENT ===')
54
+ print('\n=== ENVIRONMENT ===')
51
55
  print('OSD version: ' + str(version('opticallyshallowdeep')))
52
56
  print('Start time: ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())))
53
- print('file_in: ' + str(file_in))
57
+ print('file_L1C: ' + str(file_L1C))
58
+ print('file_L2R: ' + str(file_L2R))
54
59
  print('folder_out: ' + str(folder_out))
55
60
  print('\n=== PRE-PROCESSING ===')
56
61
 
@@ -61,23 +66,30 @@ def run(file_in,folder_out, to_log=True):
61
66
 
62
67
  ### Take input path and identify model path
63
68
 
64
- # TOA
65
- if (file_in.endswith('.safe') or file_in.endswith('.SAFE')) and 'MSIL1C' in file_in:
69
+ # If ACOLITE L2R is not provided
70
+ if file_L2R is None:
71
+
66
72
  if_SR = False
67
73
  model = 'models/TOA.h5'
68
74
  model_columns = GTOA_model_columns
75
+ file_in = file_L1C
69
76
 
70
77
  # make multiband_image
71
- image_path = make_multiband_image(file_in,folder_out)
78
+ image_path = make_multiband_image(file_L1C,folder_out)
79
+
80
+ # If ACOLITE L2R is provided
81
+ else:
82
+
83
+ if not os.path.exists(file_L2R):
84
+ sys.exit('file_L2R does not exist: ' + str(file_L2R))
72
85
 
73
- # SR
74
- elif file_in.endswith('.nc') or file_in.endswith('.NC'):
75
86
  if_SR = True
76
87
  model = 'models/SR.h5'
77
88
  model_columns = GACOLITE_model_columns
89
+ file_in = file_L2R
78
90
 
79
91
  # make multiband_image
80
- image_path = netcdf_to_multiband_geotiff(file_in, folder_out)
92
+ image_path = netcdf_to_multiband_geotiff(file_L2R, folder_out)
81
93
 
82
94
  # make it a list of lists
83
95
  selected_columns = [parse_string(s) for s in model_columns]
@@ -90,15 +102,21 @@ def run(file_in,folder_out, to_log=True):
90
102
 
91
103
  # check
92
104
  image=check_transpose(image)
105
+
106
+ # make cloud mask
107
+ img_cloud = cloud_mask(file_L1C)
108
+ cloud_list = make_vertical_strips(img_cloud)
93
109
 
94
110
  print('\n=== PREDICTING OSW/ODW ===')
95
111
 
96
112
  # create strips and process them -- make big RGB image
97
- RGB_img=process_as_strips(image, image_path, if_SR, model_path, selected_columns, model_columns, file_in)
113
+ RGB_img=process_as_strips(image, image_path, if_SR, model_path, selected_columns, model_columns, file_in, cloud_list)
98
114
 
99
115
  # write as geotiff
100
116
  write_georef_image(image_path,RGB_img)
101
- print("Image OSW/ODW completed {}".format(RGB_img.shape))
117
+ print("Image OSW/ODW completed, dimension: {}".format(RGB_img.shape))
118
+
119
+ print('Finish time: ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())))
102
120
 
103
121
  del RGB_img
104
122
  gc.collect()
@@ -0,0 +1,27 @@
1
+
2
+ import rasterio, gc
3
+ import numpy as np
4
+
5
+ def write_georef_image(image_path,RGB_img):
6
+
7
+ output_name = image_path.replace('.tif','_OSW_ODW.tif')
8
+ raster_with_ref = rasterio.open(image_path) # Open the raster with geospatial information
9
+ crs = raster_with_ref.crs # Get the CRS (Coordinate Reference System) from the raster
10
+ epsg_from_raster = crs.to_epsg() # Use the EPSG code from the CRS
11
+ height, width, _ = RGB_img.shape
12
+
13
+ count = 1
14
+ dtype = RGB_img.dtype
15
+ transform = raster_with_ref.transform# Use the same transform as the reference raster
16
+
17
+ output_band = RGB_img[:,:,1]
18
+ mask_band = RGB_img[:,:,2]
19
+ output_band[mask_band == 0] = 255
20
+
21
+ with rasterio.open(output_name, "w",driver="GTiff", height=height, width=width, nodata=255,
22
+ count=count, dtype=dtype, crs=crs, transform=transform) as dst:
23
+ dst.write(output_band, 1)
24
+
25
+ del raster_with_ref
26
+ gc.collect()
27
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: opticallyshallowdeep
3
- Version: 1.1.3
3
+ Version: 1.2.0
4
4
  Summary: Identify optically shallow and deep waters in satellite imagery
5
5
  Author: Yulun Wu
6
6
  Author-email: yulunwu8@gmail.com
@@ -16,11 +16,12 @@ Requires-Dist: pyproj
16
16
  Requires-Dist: joblib
17
17
  Requires-Dist: scipy
18
18
  Requires-Dist: matplotlib
19
+ Requires-Dist: imagecodecs
19
20
  Requires-Dist: tensorflow
20
21
 
21
22
  # Optically-Shallow-Deep
22
23
 
23
- This python tool delineates optically shallow and deep waters in Sentinel-2 imagery. The tool uses a deep neural network that was trained on a diverse set of global images.
24
+ This python tool delineates optically shallow and deep waters in Sentinel-2 imagery. The tool uses a deep neural network (DNN) that was trained on a diverse set of global images.
24
25
 
25
26
  Supported input includes L1C SAFE files and ACOLITE-processed L2R netCDF files. The output geotiff contains probabilities of water pixels being optically shallow and deep.
26
27
 
@@ -77,24 +78,48 @@ pip3 install opticallyshallowdeep
77
78
 
78
79
  ## Quick Start
79
80
 
81
+ For L1C files:
82
+
80
83
  ```python
81
84
  import opticallyshallowdeep as osd
82
85
 
83
86
  # Input file
84
- file_in = 'test_folder_in/S2.SAFE' # or path to an ACOLTIE-generated L2R netCDF file
87
+ file_L1C = 'folder/S2.SAFE'
88
+
89
+ # Output folder
90
+ folder_out = 'folder/test_folder_out'
91
+
92
+ # Run the OSW/ODW classifier
93
+ osd.run(file_L1C, folder_out)
94
+ ```
95
+
96
+ For ACOLITE L2R files:
97
+
98
+ ```python
99
+ import opticallyshallowdeep as osd
100
+
101
+ # Input files
102
+ file_L1C = 'test_folder_in/S2.SAFE'
103
+ file_L2R = 'test_folder_in/L2R.nc'
85
104
 
86
105
  # Output folder
87
106
  folder_out = 'folder/test_folder_out'
88
107
 
89
108
  # Run the OSW/ODW classifier
90
- osd.run(file_in, folder_out)
109
+ osd.run(file_in, folder_out, file_L2R=file_L2R)
91
110
  ```
92
111
 
112
+ The L1C file is always required as it contains a cloud mask. Pixels within 8 pixels of the cloud mask are masked to reduce the impact of clouds.
93
113
 
94
- Output is a 3-band geotiff:
95
114
 
96
- - B1: Binary prediction (OSW/ODW)
97
- - B2: Prediction probability of OSW (100 means most likely OSW, 0 means most likely ODW)
98
- - B3: pixels that are masked out
115
+ Output is a 1-band geotiff, with values of prediction probability of OSW (100 means most likely OSW, 0 means most likely ODW). Non-water pixels are masked. It is recommended to use pixels between 0 and 40 as ODW, and pixels between 60 and 100 as OSW (publication in review).
99
116
 
100
117
  A log file, an intermediate multi-band geotiff, and a preview PNG are also generated in the output folder. They can be deleted after the processing.
118
+
119
+ ## Training, test, and validation data
120
+
121
+ All annotated shapefiles used in training, testing, and validating the DNN model are in the annotated_shapefiles folder, grouped by Sentinel-2 Scene ID.
122
+
123
+
124
+
125
+
@@ -4,8 +4,10 @@ README.md
4
4
  setup.py
5
5
  opticallyshallowdeep/__init__.py
6
6
  opticallyshallowdeep/check_transpose.py
7
+ opticallyshallowdeep/cloud_mask.py
7
8
  opticallyshallowdeep/find_epsg.py
8
9
  opticallyshallowdeep/make_multiband_image.py
10
+ opticallyshallowdeep/make_vertical_strips.py
9
11
  opticallyshallowdeep/netcdf_to_multiband_geotiff.py
10
12
  opticallyshallowdeep/parse_string.py
11
13
  opticallyshallowdeep/process_as_strips.py
@@ -6,4 +6,5 @@ pyproj
6
6
  joblib
7
7
  scipy
8
8
  matplotlib
9
+ imagecodecs
9
10
  tensorflow
@@ -5,7 +5,7 @@ with open("readme.md", "r") as fh:
5
5
 
6
6
  setup(
7
7
  name='opticallyshallowdeep',
8
- version='1.1.3',
8
+ version='1.2.0',
9
9
  author='Yulun Wu',
10
10
  author_email='yulunwu8@gmail.com',
11
11
  description='Identify optically shallow and deep waters in satellite imagery',
@@ -19,7 +19,7 @@ setup(
19
19
  ],
20
20
  python_requires='>=3.8',
21
21
  install_requires=['geopandas','rasterio','tifffile','netCDF4','pyproj',
22
- 'joblib','scipy','matplotlib','tensorflow']
22
+ 'joblib','scipy','matplotlib','imagecodecs','tensorflow']
23
23
  )
24
24
 
25
25
 
@@ -1,21 +0,0 @@
1
-
2
- import rasterio, gc
3
- import numpy as np
4
-
5
- def write_georef_image(image_path,RGB_img):
6
-
7
- output_name = image_path.replace('.tif','_OSW_ODW.tif')
8
- raster_with_ref = rasterio.open(image_path) # Open the raster with geospatial information
9
- crs = raster_with_ref.crs#Get the CRS (Coordinate Reference System) from the raster
10
- epsg_from_raster = crs.to_epsg()#Use the EPSG code from the CRS
11
- height, width, _ = RGB_img.shape
12
- count = 3 #3 bands for all the
13
- dtype = RGB_img.dtype
14
- transform = raster_with_ref.transform# Use the same transform as the reference raster
15
- with rasterio.open(output_name, "w",driver="GTiff",height=height,width=width,
16
- count=count,dtype=dtype,crs=crs,transform=transform) as dst:
17
- dst.write(np.moveaxis(RGB_img, -1, 0))
18
- del raster_with_ref
19
- gc.collect()
20
-
21
-