opticallyshallowdeep 1.1.5__tar.gz → 1.2.1__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 (26) hide show
  1. {opticallyshallowdeep-1.1.5/opticallyshallowdeep.egg-info → opticallyshallowdeep-1.2.1}/PKG-INFO +40 -18
  2. opticallyshallowdeep-1.2.1/README.md +102 -0
  3. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/opticallyshallowdeep/check_transpose.py +2 -2
  4. opticallyshallowdeep-1.2.1/opticallyshallowdeep/cloud_mask.py +103 -0
  5. opticallyshallowdeep-1.2.1/opticallyshallowdeep/make_vertical_strips.py +34 -0
  6. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/opticallyshallowdeep/netcdf_to_multiband_geotiff.py +1 -1
  7. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/opticallyshallowdeep/process_as_strips.py +29 -30
  8. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/opticallyshallowdeep/run.py +32 -12
  9. opticallyshallowdeep-1.2.1/opticallyshallowdeep/write_georef_image.py +27 -0
  10. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1/opticallyshallowdeep.egg-info}/PKG-INFO +40 -18
  11. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/opticallyshallowdeep.egg-info/SOURCES.txt +2 -0
  12. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/setup.py +1 -1
  13. opticallyshallowdeep-1.1.5/README.md +0 -80
  14. opticallyshallowdeep-1.1.5/opticallyshallowdeep/write_georef_image.py +0 -21
  15. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/LICENSE +0 -0
  16. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/MANIFEST.in +0 -0
  17. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/opticallyshallowdeep/__init__.py +0 -0
  18. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/opticallyshallowdeep/find_epsg.py +0 -0
  19. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/opticallyshallowdeep/make_multiband_image.py +0 -0
  20. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/opticallyshallowdeep/models/SR.h5 +0 -0
  21. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/opticallyshallowdeep/models/TOA.h5 +0 -0
  22. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/opticallyshallowdeep/parse_string.py +0 -0
  23. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/opticallyshallowdeep.egg-info/dependency_links.txt +0 -0
  24. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/opticallyshallowdeep.egg-info/requires.txt +0 -0
  25. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/opticallyshallowdeep.egg-info/top_level.txt +0 -0
  26. {opticallyshallowdeep-1.1.5 → opticallyshallowdeep-1.2.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: opticallyshallowdeep
3
- Version: 1.1.5
3
+ Version: 1.2.1
4
4
  Summary: Identify optically shallow and deep waters in satellite imagery
5
5
  Author: Yulun Wu
6
6
  Author-email: yulunwu8@gmail.com
@@ -21,9 +21,9 @@ Requires-Dist: tensorflow
21
21
 
22
22
  # Optically-Shallow-Deep
23
23
 
24
- 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.
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
+ Supported input includes Level-1C (L1C) SAFE files and ACOLITE-processed L2R netCDF files. The output geotiff contains probabilities of water pixels being optically shallow and deep.
27
27
 
28
28
  Originally coded by by Galen Richardson and Anders Knudby, modified and packaged by Yulun Wu
29
29
 
@@ -47,7 +47,6 @@ For mac OS:
47
47
  ```bash
48
48
  conda install -c apple tensorflow-deps
49
49
  python -m pip install tensorflow-macos
50
-
51
50
  ```
52
51
 
53
52
 
@@ -55,14 +54,6 @@ For windows:
55
54
 
56
55
  ```bash
57
56
  pip3 install tensorflow==2.13.0
58
-
59
- ```
60
-
61
- In case of compatibility issues, please try the newest version of tensorflow:
62
-
63
- ```bash
64
- pip3 install --upgrade --force-reinstall tensorflow
65
-
66
57
  ```
67
58
 
68
59
 
@@ -78,24 +69,55 @@ pip3 install opticallyshallowdeep
78
69
 
79
70
  ## Quick Start
80
71
 
72
+ For L1C files:
73
+
81
74
  ```python
82
75
  import opticallyshallowdeep as osd
83
76
 
84
77
  # Input file
85
- file_in = 'test_folder_in/S2.SAFE' # or path to an ACOLTIE-generated L2R netCDF file
78
+ file_L1C = 'folder/S2.SAFE'
86
79
 
87
80
  # Output folder
88
81
  folder_out = 'folder/test_folder_out'
89
82
 
90
83
  # Run the OSW/ODW classifier
91
- osd.run(file_in, folder_out)
84
+ osd.run(file_L1C, folder_out)
92
85
  ```
93
86
 
87
+ For ACOLITE L2R files:
94
88
 
95
- Output is a 3-band geotiff:
89
+ ```python
90
+ import opticallyshallowdeep as osd
96
91
 
97
- - B1: Binary prediction (OSW/ODW)
98
- - B2: Prediction probability of OSW (100 means most likely OSW, 0 means most likely ODW)
99
- - B3: pixels that are masked out
92
+ # Input files
93
+ file_L1C = 'test_folder_in/S2.SAFE'
94
+ file_L2R = 'test_folder_in/L2R.nc'
95
+
96
+ # Output folder
97
+ folder_out = 'folder/test_folder_out'
98
+
99
+ # Run the OSW/ODW classifier
100
+ osd.run(file_L1C, folder_out, file_L2R=file_L2R)
101
+ ```
102
+
103
+ The L1C file is always required as it contains a built-in cloud mask. Pixels within 8 pixels of the cloud mask are masked to reduce the impact of clouds.
104
+
105
+
106
+ Output is a 1-band geotiff, with values of prediction probability of optically shallow water (OSW): 100 means most likely OSW, 0 means most likely optically deep water (ODW). Non-water pixels are masked. It is recommended to treat pixels between 0 and 40 as ODW, and pixels between 60 and 100 as OSW (publication in review).
100
107
 
101
108
  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.
109
+
110
+
111
+ **Sample Sentinel-2 scene and output:**
112
+
113
+ <img src="images/TOA.jpeg" height="500">
114
+
115
+ <img src="images/OSW.jpeg" height="500">
116
+
117
+ ## Training, test, and validation data
118
+
119
+ All annotated shapefiles used in training, testing, and validating the DNN model are in the annotated_shapefiles folder, grouped by Sentinel-2 Scene ID.
120
+
121
+
122
+
123
+
@@ -0,0 +1,102 @@
1
+ # Optically-Shallow-Deep
2
+
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
+
5
+ Supported input includes Level-1C (L1C) SAFE files and ACOLITE-processed L2R netCDF files. The output geotiff contains probabilities of water pixels being optically shallow and deep.
6
+
7
+ Originally coded by by Galen Richardson and Anders Knudby, modified and packaged by Yulun Wu
8
+
9
+ Home page: <a href="https://github.com/yulunwu8/Optically-Shallow-Deep" target="_blank">https://github.com/yulunwu8/Optically-Shallow-Deep</a>
10
+
11
+
12
+
13
+ ## Installation
14
+
15
+ **1 - Create a conda environment and activate it:**
16
+
17
+ ```bash
18
+ conda create --name opticallyshallowdeep python=3.10
19
+ conda activate opticallyshallowdeep
20
+ ```
21
+
22
+ **2 - Install tensorflow**
23
+
24
+ For mac OS:
25
+
26
+ ```bash
27
+ conda install -c apple tensorflow-deps
28
+ python -m pip install tensorflow-macos
29
+ ```
30
+
31
+
32
+ For windows:
33
+
34
+ ```bash
35
+ pip3 install tensorflow==2.13.0
36
+ ```
37
+
38
+
39
+ For Linux and more on installing tensorflow: [https://www.tensorflow.org/install](https://www.tensorflow.org/install)
40
+
41
+
42
+ **3 - Install opticallyshallowdeep:**
43
+
44
+ ```bash
45
+ pip3 install opticallyshallowdeep
46
+ ```
47
+
48
+
49
+ ## Quick Start
50
+
51
+ For L1C files:
52
+
53
+ ```python
54
+ import opticallyshallowdeep as osd
55
+
56
+ # Input file
57
+ file_L1C = 'folder/S2.SAFE'
58
+
59
+ # Output folder
60
+ folder_out = 'folder/test_folder_out'
61
+
62
+ # Run the OSW/ODW classifier
63
+ osd.run(file_L1C, folder_out)
64
+ ```
65
+
66
+ For ACOLITE L2R files:
67
+
68
+ ```python
69
+ import opticallyshallowdeep as osd
70
+
71
+ # Input files
72
+ file_L1C = 'test_folder_in/S2.SAFE'
73
+ file_L2R = 'test_folder_in/L2R.nc'
74
+
75
+ # Output folder
76
+ folder_out = 'folder/test_folder_out'
77
+
78
+ # Run the OSW/ODW classifier
79
+ osd.run(file_L1C, folder_out, file_L2R=file_L2R)
80
+ ```
81
+
82
+ The L1C file is always required as it contains a built-in cloud mask. Pixels within 8 pixels of the cloud mask are masked to reduce the impact of clouds.
83
+
84
+
85
+ Output is a 1-band geotiff, with values of prediction probability of optically shallow water (OSW): 100 means most likely OSW, 0 means most likely optically deep water (ODW). Non-water pixels are masked. It is recommended to treat pixels between 0 and 40 as ODW, and pixels between 60 and 100 as OSW (publication in review).
86
+
87
+ 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.
88
+
89
+
90
+ **Sample Sentinel-2 scene and output:**
91
+
92
+ <img src="images/TOA.jpeg" height="500">
93
+
94
+ <img src="images/OSW.jpeg" height="500">
95
+
96
+ ## Training, test, and validation data
97
+
98
+ All annotated shapefiles used in training, testing, and validating the DNN model are in the annotated_shapefiles folder, grouped by Sentinel-2 Scene ID.
99
+
100
+
101
+
102
+
@@ -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
+
@@ -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
+
@@ -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,35 +16,26 @@ 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
29
  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
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
@@ -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
 
@@ -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()
@@ -182,13 +179,15 @@ def process_image_with_filters(img, selected_columns):
182
179
  for band, kernel_size, filter_type in filter_list:
183
180
 
184
181
  if filter_type is None:
185
- filtered_band = img[:, :, band].astype(np.uint16)#this means it is a single pixel
182
+ filtered_band = img[:, :, band]
186
183
  else:
187
184
  with warnings.catch_warnings():
188
185
  warnings.simplefilter("ignore", category=RuntimeWarning)
189
186
  filtered_band = apply_filter(img[:, :, band].astype(np.float32), kernel_size, filter_type)
190
187
  filtered_band[filtered_band==-32768] = 32768
191
- filtered_band = filtered_band.astype(np.uint16)
188
+
189
+ filtered_band[filtered_band<0] = 0
190
+ filtered_band = filtered_band.astype(np.uint16)#this means it is a single pixel
192
191
 
193
192
  output_bands.append(filtered_band)#append to list of filters
194
193
  del filtered_band
@@ -415,11 +414,11 @@ def plot_RGB_img(RGB_img, image_path):
415
414
  import matplotlib.pyplot as plt
416
415
  fig, ax = plt.subplots(1, 3, figsize=(10, 10), sharex=True, sharey=True)
417
416
  ax[0].imshow(RGB_img[:,:,0])#plotting to see what OSW/ODW looks like
418
- ax[0].set_title('Prediction Image')
417
+ ax[0].set_title('Prediction based on 0.5 threshold')
419
418
  ax[1].imshow(RGB_img[:,:,1])
420
- ax[1].set_title('Prediction Probability Image')
419
+ ax[1].set_title('Prediction probability')
421
420
  ax[2].imshow(RGB_img[:,:,2])
422
- ax[2].set_title('Masked Image')
421
+ ax[2].set_title('Non-water mask')
423
422
  # plt.show()
424
423
 
425
424
  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,11 @@ 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
+ if file_L2R is None:
36
+ log_base = os.path.basename(file_L1C).replace('.safe','.txt').replace('.SAFE','.txt')
37
+ else:
38
+ log_base = os.path.basename(file_L2R).replace('.nc','.txt')
39
+
32
40
  log_base = 'OSD_log_'+ log_base
33
41
  log_file = os.path.join(folder_out,log_base)
34
42
 
@@ -50,7 +58,8 @@ def run(file_in,folder_out, to_log=True):
50
58
  print('\n=== ENVIRONMENT ===')
51
59
  print('OSD version: ' + str(version('opticallyshallowdeep')))
52
60
  print('Start time: ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())))
53
- print('file_in: ' + str(file_in))
61
+ print('file_L1C: ' + str(file_L1C))
62
+ print('file_L2R: ' + str(file_L2R))
54
63
  print('folder_out: ' + str(folder_out))
55
64
  print('\n=== PRE-PROCESSING ===')
56
65
 
@@ -61,23 +70,30 @@ def run(file_in,folder_out, to_log=True):
61
70
 
62
71
  ### Take input path and identify model path
63
72
 
64
- # TOA
65
- if (file_in.endswith('.safe') or file_in.endswith('.SAFE')) and 'MSIL1C' in file_in:
73
+ # If ACOLITE L2R is not provided
74
+ if file_L2R is None:
75
+
66
76
  if_SR = False
67
77
  model = 'models/TOA.h5'
68
78
  model_columns = GTOA_model_columns
79
+ file_in = file_L1C
69
80
 
70
81
  # make multiband_image
71
- image_path = make_multiband_image(file_in,folder_out)
82
+ image_path = make_multiband_image(file_L1C,folder_out)
83
+
84
+ # If ACOLITE L2R is provided
85
+ else:
86
+
87
+ if not os.path.exists(file_L2R):
88
+ sys.exit('file_L2R does not exist: ' + str(file_L2R))
72
89
 
73
- # SR
74
- elif file_in.endswith('.nc') or file_in.endswith('.NC'):
75
90
  if_SR = True
76
91
  model = 'models/SR.h5'
77
92
  model_columns = GACOLITE_model_columns
93
+ file_in = file_L2R
78
94
 
79
95
  # make multiband_image
80
- image_path = netcdf_to_multiband_geotiff(file_in, folder_out)
96
+ image_path = netcdf_to_multiband_geotiff(file_L2R, folder_out)
81
97
 
82
98
  # make it a list of lists
83
99
  selected_columns = [parse_string(s) for s in model_columns]
@@ -90,11 +106,15 @@ def run(file_in,folder_out, to_log=True):
90
106
 
91
107
  # check
92
108
  image=check_transpose(image)
109
+
110
+ # make cloud mask
111
+ img_cloud = cloud_mask(file_L1C)
112
+ cloud_list = make_vertical_strips(img_cloud)
93
113
 
94
114
  print('\n=== PREDICTING OSW/ODW ===')
95
115
 
96
116
  # 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)
117
+ RGB_img=process_as_strips(image, image_path, if_SR, model_path, selected_columns, model_columns, file_in, cloud_list)
98
118
 
99
119
  # write as geotiff
100
120
  write_georef_image(image_path,RGB_img)
@@ -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.5
3
+ Version: 1.2.1
4
4
  Summary: Identify optically shallow and deep waters in satellite imagery
5
5
  Author: Yulun Wu
6
6
  Author-email: yulunwu8@gmail.com
@@ -21,9 +21,9 @@ Requires-Dist: tensorflow
21
21
 
22
22
  # Optically-Shallow-Deep
23
23
 
24
- 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.
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
+ Supported input includes Level-1C (L1C) SAFE files and ACOLITE-processed L2R netCDF files. The output geotiff contains probabilities of water pixels being optically shallow and deep.
27
27
 
28
28
  Originally coded by by Galen Richardson and Anders Knudby, modified and packaged by Yulun Wu
29
29
 
@@ -47,7 +47,6 @@ For mac OS:
47
47
  ```bash
48
48
  conda install -c apple tensorflow-deps
49
49
  python -m pip install tensorflow-macos
50
-
51
50
  ```
52
51
 
53
52
 
@@ -55,14 +54,6 @@ For windows:
55
54
 
56
55
  ```bash
57
56
  pip3 install tensorflow==2.13.0
58
-
59
- ```
60
-
61
- In case of compatibility issues, please try the newest version of tensorflow:
62
-
63
- ```bash
64
- pip3 install --upgrade --force-reinstall tensorflow
65
-
66
57
  ```
67
58
 
68
59
 
@@ -78,24 +69,55 @@ pip3 install opticallyshallowdeep
78
69
 
79
70
  ## Quick Start
80
71
 
72
+ For L1C files:
73
+
81
74
  ```python
82
75
  import opticallyshallowdeep as osd
83
76
 
84
77
  # Input file
85
- file_in = 'test_folder_in/S2.SAFE' # or path to an ACOLTIE-generated L2R netCDF file
78
+ file_L1C = 'folder/S2.SAFE'
86
79
 
87
80
  # Output folder
88
81
  folder_out = 'folder/test_folder_out'
89
82
 
90
83
  # Run the OSW/ODW classifier
91
- osd.run(file_in, folder_out)
84
+ osd.run(file_L1C, folder_out)
92
85
  ```
93
86
 
87
+ For ACOLITE L2R files:
94
88
 
95
- Output is a 3-band geotiff:
89
+ ```python
90
+ import opticallyshallowdeep as osd
96
91
 
97
- - B1: Binary prediction (OSW/ODW)
98
- - B2: Prediction probability of OSW (100 means most likely OSW, 0 means most likely ODW)
99
- - B3: pixels that are masked out
92
+ # Input files
93
+ file_L1C = 'test_folder_in/S2.SAFE'
94
+ file_L2R = 'test_folder_in/L2R.nc'
95
+
96
+ # Output folder
97
+ folder_out = 'folder/test_folder_out'
98
+
99
+ # Run the OSW/ODW classifier
100
+ osd.run(file_L1C, folder_out, file_L2R=file_L2R)
101
+ ```
102
+
103
+ The L1C file is always required as it contains a built-in cloud mask. Pixels within 8 pixels of the cloud mask are masked to reduce the impact of clouds.
104
+
105
+
106
+ Output is a 1-band geotiff, with values of prediction probability of optically shallow water (OSW): 100 means most likely OSW, 0 means most likely optically deep water (ODW). Non-water pixels are masked. It is recommended to treat pixels between 0 and 40 as ODW, and pixels between 60 and 100 as OSW (publication in review).
100
107
 
101
108
  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.
109
+
110
+
111
+ **Sample Sentinel-2 scene and output:**
112
+
113
+ <img src="images/TOA.jpeg" height="500">
114
+
115
+ <img src="images/OSW.jpeg" height="500">
116
+
117
+ ## Training, test, and validation data
118
+
119
+ All annotated shapefiles used in training, testing, and validating the DNN model are in the annotated_shapefiles folder, grouped by Sentinel-2 Scene ID.
120
+
121
+
122
+
123
+
@@ -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
@@ -5,7 +5,7 @@ with open("readme.md", "r") as fh:
5
5
 
6
6
  setup(
7
7
  name='opticallyshallowdeep',
8
- version='1.1.5',
8
+ version='1.2.1',
9
9
  author='Yulun Wu',
10
10
  author_email='yulunwu8@gmail.com',
11
11
  description='Identify optically shallow and deep waters in satellite imagery',
@@ -1,80 +0,0 @@
1
- # Optically-Shallow-Deep
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.
4
-
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
-
7
- Originally coded by by Galen Richardson and Anders Knudby, modified and packaged by Yulun Wu
8
-
9
- Home page: <a href="https://github.com/yulunwu8/Optically-Shallow-Deep" target="_blank">https://github.com/yulunwu8/Optically-Shallow-Deep</a>
10
-
11
-
12
-
13
- ## Installation
14
-
15
- **1 - Create a conda environment and activate it:**
16
-
17
- ```bash
18
- conda create --name opticallyshallowdeep python=3.10
19
- conda activate opticallyshallowdeep
20
- ```
21
-
22
- **2 - Install tensorflow**
23
-
24
- For mac OS:
25
-
26
- ```bash
27
- conda install -c apple tensorflow-deps
28
- python -m pip install tensorflow-macos
29
-
30
- ```
31
-
32
-
33
- For windows:
34
-
35
- ```bash
36
- pip3 install tensorflow==2.13.0
37
-
38
- ```
39
-
40
- In case of compatibility issues, please try the newest version of tensorflow:
41
-
42
- ```bash
43
- pip3 install --upgrade --force-reinstall tensorflow
44
-
45
- ```
46
-
47
-
48
- For Linux and more on installing tensorflow: [https://www.tensorflow.org/install](https://www.tensorflow.org/install)
49
-
50
-
51
- **3 - Install opticallyshallowdeep:**
52
-
53
- ```bash
54
- pip3 install opticallyshallowdeep
55
- ```
56
-
57
-
58
- ## Quick Start
59
-
60
- ```python
61
- import opticallyshallowdeep as osd
62
-
63
- # Input file
64
- file_in = 'test_folder_in/S2.SAFE' # or path to an ACOLTIE-generated L2R netCDF file
65
-
66
- # Output folder
67
- folder_out = 'folder/test_folder_out'
68
-
69
- # Run the OSW/ODW classifier
70
- osd.run(file_in, folder_out)
71
- ```
72
-
73
-
74
- Output is a 3-band geotiff:
75
-
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
79
-
80
- 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.
@@ -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
-