[feat] (backend) add nextzen terrain xyz provider as DEM raster source, make sam predictions with RGB image made with normalized slope, elevation and curvature as channels
Browse files- samgis/io/raster_helpers.py +291 -0
- samgis/io/{lambda_helpers.py → wrappers_helpers.py} +27 -6
- samgis/prediction_api/predictors.py +15 -3
- samgis/prediction_api/sam_onnx.py +3 -2
- samgis/utilities/constants.py +11 -3
- samgis/utilities/type_hints.py +13 -3
- wrappers/fastapi_wrapper.py +1 -1
- wrappers/lambda_wrapper.py +1 -1
samgis/io/raster_helpers.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""helpers for computer vision duties"""
|
| 2 |
+
import numpy as np
|
| 3 |
+
from numpy import ndarray
|
| 4 |
+
|
| 5 |
+
from samgis import app_logger
|
| 6 |
+
from samgis.utilities.type_hints import TmsTerrainProvidersNames
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def get_nextzen_terrain_rgb_formula(red: ndarray, green: ndarray, blue: ndarray) -> ndarray:
|
| 10 |
+
"""
|
| 11 |
+
Compute a 32-bits 2d digital elevation model from a nextzen 'terrarium' (terrain-rgb) raster.
|
| 12 |
+
'Terrarium' format PNG tiles contain raw elevation data in meters, in Mercator projection (EPSG:3857).
|
| 13 |
+
All values are positive with a 32,768 offset, split into the red, green, and blue channels,
|
| 14 |
+
with 16 bits of integer and 8 bits of fraction. To decode:
|
| 15 |
+
|
| 16 |
+
(red * 256 + green + blue / 256) - 32768
|
| 17 |
+
|
| 18 |
+
More details on https://www.mapzen.com/blog/elevation/
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
red: red-valued channel image array
|
| 22 |
+
green: green-valued channel image array
|
| 23 |
+
blue: blue-valued channel image array
|
| 24 |
+
|
| 25 |
+
Returns:
|
| 26 |
+
ndarray: nextzen 'terrarium' 2d digital elevation model raster at 32 bits
|
| 27 |
+
|
| 28 |
+
"""
|
| 29 |
+
return (red * 256 + green + blue / 256) - 32768
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def get_mapbox__terrain_rgb_formula(red: ndarray, green: ndarray, blue: ndarray) -> ndarray:
|
| 33 |
+
return ((red * 256 * 256 + green * 256 + blue) * 0.1) - 10000
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
providers_terrain_rgb_formulas = {
|
| 37 |
+
TmsTerrainProvidersNames.MAPBOX_TERRAIN_TILES_NAME: get_mapbox__terrain_rgb_formula,
|
| 38 |
+
TmsTerrainProvidersNames.NEXTZEN_TERRAIN_TILES_NAME: get_nextzen_terrain_rgb_formula
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _get_2d_array_from_3d(arr: ndarray) -> ndarray:
|
| 43 |
+
return arr.reshape(arr.shape[0], arr.shape[1])
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _channel_split(arr: ndarray) -> list[ndarray]:
|
| 47 |
+
from numpy import dsplit
|
| 48 |
+
|
| 49 |
+
return dsplit(arr, arr.shape[-1])
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def get_raster_terrain_rgb_like(arr: ndarray, xyz_provider_name, nan_value_int: int = -12000):
|
| 53 |
+
"""
|
| 54 |
+
Compute a 32-bits 2d digital elevation model from a terrain-rgb raster.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
arr: rgb raster
|
| 58 |
+
xyz_provider_name: xyz provider
|
| 59 |
+
nan_value_int: threshold int value to replace NaN
|
| 60 |
+
|
| 61 |
+
Returns:
|
| 62 |
+
ndarray: 2d digital elevation model raster at 32 bits
|
| 63 |
+
"""
|
| 64 |
+
red, green, blue = _channel_split(arr)
|
| 65 |
+
dem_rgb = providers_terrain_rgb_formulas[xyz_provider_name](red, green, blue)
|
| 66 |
+
output = _get_2d_array_from_3d(dem_rgb)
|
| 67 |
+
output[output < nan_value_int] = np.NaN
|
| 68 |
+
return output
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def get_rgb_prediction_image(raster_cropped: ndarray, slope_cellsize: int, invert_image: bool = True) -> ndarray:
|
| 72 |
+
"""
|
| 73 |
+
Return an RGB image from input numpy array
|
| 74 |
+
|
| 75 |
+
Args:
|
| 76 |
+
raster_cropped: input numpy array
|
| 77 |
+
slope_cellsize: window size to calculate slope and curvature (1st and 2nd degree array derivative)
|
| 78 |
+
invert_image:
|
| 79 |
+
|
| 80 |
+
Returns:
|
| 81 |
+
tuple of str: image filename, image path (with filename)
|
| 82 |
+
"""
|
| 83 |
+
from samgis.utilities.constants import CHANNEL_EXAGGERATIONS_LIST
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
slope, curvature = get_slope_curvature(raster_cropped, slope_cellsize=slope_cellsize)
|
| 87 |
+
channel0 = raster_cropped
|
| 88 |
+
channel1 = normalize_array_list(
|
| 89 |
+
[raster_cropped, slope, curvature], CHANNEL_EXAGGERATIONS_LIST, title=f"channel1_normlist")
|
| 90 |
+
channel2 = curvature
|
| 91 |
+
|
| 92 |
+
return get_rgb_image(channel0, channel1, channel2, invert_image=invert_image)
|
| 93 |
+
except ValueError as ve_get_rgb_prediction_image:
|
| 94 |
+
msg = f"ve_get_rgb_prediction_image:{ve_get_rgb_prediction_image}."
|
| 95 |
+
app_logger.error(msg)
|
| 96 |
+
raise ve_get_rgb_prediction_image
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def get_rgb_image(arr_channel0: ndarray, arr_channel1: ndarray, arr_channel2: ndarray,
|
| 100 |
+
invert_image: bool = True) -> ndarray:
|
| 101 |
+
"""
|
| 102 |
+
Return an RGB image from input R,G,B channel arrays
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
arr_channel0: channel image 0
|
| 106 |
+
arr_channel1: channel image 1
|
| 107 |
+
arr_channel2: channel image 2
|
| 108 |
+
invert_image: invert the RGB image channel order
|
| 109 |
+
|
| 110 |
+
Returns:
|
| 111 |
+
ndarray: RGB image
|
| 112 |
+
|
| 113 |
+
"""
|
| 114 |
+
try:
|
| 115 |
+
# RED curvature, GREEN slope, BLUE dem, invert_image=True
|
| 116 |
+
if len(arr_channel0.shape) != 2:
|
| 117 |
+
msg = f"arr_size, wrong type:{type(arr_channel0)} or arr_size:{arr_channel0.shape}."
|
| 118 |
+
app_logger.error(msg)
|
| 119 |
+
raise ValueError(msg)
|
| 120 |
+
data_rgb = np.zeros((arr_channel0.shape[0], arr_channel0.shape[1], 3), dtype=np.uint8)
|
| 121 |
+
app_logger.debug(f"arr_container data_rgb, type:{type(data_rgb)}, arr_shape:{data_rgb.shape}.")
|
| 122 |
+
data_rgb[:, :, 0] = normalize_array(
|
| 123 |
+
arr_channel0.astype(float), high=1, norm_type="float", title=f"RGB:channel0") * 64
|
| 124 |
+
data_rgb[:, :, 1] = normalize_array(
|
| 125 |
+
arr_channel1.astype(float), high=1, norm_type="float", title=f"RGB:channel1") * 128
|
| 126 |
+
data_rgb[:, :, 2] = normalize_array(
|
| 127 |
+
arr_channel2.astype(float), high=1, norm_type="float", title=f"RGB:channel2") * 192
|
| 128 |
+
if invert_image:
|
| 129 |
+
data_rgb = np.bitwise_not(data_rgb)
|
| 130 |
+
return data_rgb
|
| 131 |
+
except ValueError as ve_get_rgb_image:
|
| 132 |
+
msg = f"ve_get_rgb_image:{ve_get_rgb_image}."
|
| 133 |
+
app_logger.error(msg)
|
| 134 |
+
raise ve_get_rgb_image
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def get_slope_curvature(dem: ndarray, slope_cellsize: int, title: str = "") -> tuple[ndarray, ndarray]:
|
| 138 |
+
"""
|
| 139 |
+
Return a tuple of two numpy arrays representing slope and curvature (1st grade derivative and 2nd grade derivative)
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
dem: input numpy array
|
| 143 |
+
slope_cellsize: window size to calculate slope and curvature
|
| 144 |
+
title: array name
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
tuple of ndarrays: slope image, curvature image
|
| 148 |
+
|
| 149 |
+
"""
|
| 150 |
+
|
| 151 |
+
app_logger.info(f"dem shape:{dem.shape}, slope_cellsize:{slope_cellsize}.")
|
| 152 |
+
|
| 153 |
+
try:
|
| 154 |
+
dem = dem.astype(float)
|
| 155 |
+
app_logger.debug("get_slope_curvature:: start")
|
| 156 |
+
slope = calculate_slope(dem, slope_cellsize)
|
| 157 |
+
app_logger.debug("get_slope_curvature:: created slope raster")
|
| 158 |
+
s2c = calculate_slope(slope, slope_cellsize)
|
| 159 |
+
curvature = normalize_array(s2c, norm_type="float", title=f"SC:curvature_{title}")
|
| 160 |
+
app_logger.debug("get_slope_curvature:: created curvature raster")
|
| 161 |
+
|
| 162 |
+
return slope, curvature
|
| 163 |
+
except ValueError as ve_get_slope_curvature:
|
| 164 |
+
msg = f"ve_get_slope_curvature:{ve_get_slope_curvature}."
|
| 165 |
+
app_logger.error(msg)
|
| 166 |
+
raise ve_get_slope_curvature
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def calculate_slope(dem_array: ndarray, cell_size: int, calctype: str = "degree") -> ndarray:
|
| 170 |
+
"""
|
| 171 |
+
Return a numpy array representing slope (1st grade derivative)
|
| 172 |
+
|
| 173 |
+
Args:
|
| 174 |
+
dem_array: input numpy array
|
| 175 |
+
cell_size: window size to calculate slope
|
| 176 |
+
calctype: calculus type
|
| 177 |
+
|
| 178 |
+
Returns:
|
| 179 |
+
ndarray: slope image
|
| 180 |
+
|
| 181 |
+
"""
|
| 182 |
+
|
| 183 |
+
try:
|
| 184 |
+
gradx, grady = np.gradient(dem_array, cell_size)
|
| 185 |
+
dem_slope = np.sqrt(gradx ** 2 + grady ** 2)
|
| 186 |
+
if calctype == "degree":
|
| 187 |
+
dem_slope = np.degrees(np.arctan(dem_slope))
|
| 188 |
+
app_logger.debug(f"extracted slope with calctype:{calctype}.")
|
| 189 |
+
return dem_slope
|
| 190 |
+
except ValueError as ve_calculate_slope:
|
| 191 |
+
msg = f"ve_calculate_slope:{ve_calculate_slope}."
|
| 192 |
+
app_logger.error(msg)
|
| 193 |
+
raise ve_calculate_slope
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def normalize_array(arr: ndarray, high: int = 255, norm_type: str = "float", invert: bool = False, title: str = "") -> ndarray:
|
| 197 |
+
"""
|
| 198 |
+
Return normalized numpy array between 0 and 'high' value. Default normalization type is int
|
| 199 |
+
|
| 200 |
+
Args:
|
| 201 |
+
arr: input numpy array
|
| 202 |
+
high: max value to use for normalization
|
| 203 |
+
norm_type: type of normalization: could be 'float' or 'int'
|
| 204 |
+
invert: bool to choose if invert the normalized numpy array
|
| 205 |
+
title: array title name
|
| 206 |
+
|
| 207 |
+
Returns:
|
| 208 |
+
ndarray: normalized numpy array
|
| 209 |
+
|
| 210 |
+
"""
|
| 211 |
+
|
| 212 |
+
h_min_arr = np.nanmin(arr)
|
| 213 |
+
h_arr_max = np.nanmax(arr)
|
| 214 |
+
try:
|
| 215 |
+
h_diff = h_arr_max - h_min_arr
|
| 216 |
+
app_logger.debug(
|
| 217 |
+
f"normalize_array:: '{title}',h_min_arr:{h_min_arr},h_arr_max:{h_arr_max},h_diff:{h_diff}, dtype:{arr.dtype}.")
|
| 218 |
+
except Exception as e_h_diff:
|
| 219 |
+
app_logger.error(f"e_h_diff:{e_h_diff}.")
|
| 220 |
+
raise e_h_diff
|
| 221 |
+
|
| 222 |
+
if check_empty_array(arr, high) or check_empty_array(arr, h_diff):
|
| 223 |
+
msg_ve = f"normalize_array::empty array '{title}',h_min_arr:{h_min_arr},h_arr_max:{h_arr_max},h_diff:{h_diff}, dtype:{arr.dtype}."
|
| 224 |
+
app_logger.error(msg_ve)
|
| 225 |
+
raise ValueError(msg_ve)
|
| 226 |
+
try:
|
| 227 |
+
normalized = high * (arr - h_min_arr) / h_diff
|
| 228 |
+
normalized = np.nanmax(normalized) - normalized if invert else normalized
|
| 229 |
+
return normalized.astype(int) if norm_type == "int" else normalized
|
| 230 |
+
except FloatingPointError as fe:
|
| 231 |
+
msg = f"normalize_array::{title}:h_arr_max:{h_arr_max},h_min_arr:{h_min_arr},fe:{fe}."
|
| 232 |
+
app_logger.error(msg)
|
| 233 |
+
raise ValueError(msg)
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def normalize_array_list(arr_list: list[ndarray], exaggerations_list: list[float] = None, title: str = "") -> ndarray:
|
| 237 |
+
"""
|
| 238 |
+
Return a normalized numpy array from a list of numpy array and an optional list of exaggeration values.
|
| 239 |
+
|
| 240 |
+
Args:
|
| 241 |
+
arr_list: list of array to use for normalization
|
| 242 |
+
exaggerations_list: list of exaggeration values
|
| 243 |
+
title: array title name
|
| 244 |
+
|
| 245 |
+
Returns:
|
| 246 |
+
ndarray: normalized numpy array
|
| 247 |
+
|
| 248 |
+
"""
|
| 249 |
+
|
| 250 |
+
if not arr_list:
|
| 251 |
+
msg = f"input list can't be empty:{arr_list}."
|
| 252 |
+
app_logger.error(msg)
|
| 253 |
+
raise ValueError(msg)
|
| 254 |
+
if exaggerations_list is None:
|
| 255 |
+
exaggerations_list = list(np.ones(len(arr_list)))
|
| 256 |
+
arr_tmp = np.zeros(arr_list[0].shape)
|
| 257 |
+
for a, exaggeration in zip(arr_list, exaggerations_list):
|
| 258 |
+
app_logger.debug(f"normalize_array_list::exaggeration:{exaggeration}.")
|
| 259 |
+
arr_tmp += normalize_array(a, norm_type="float", title=f"ARRLIST:{title}.") * exaggeration
|
| 260 |
+
return arr_tmp / len(arr_list)
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def check_empty_array(arr: ndarray, val: float) -> bool:
|
| 264 |
+
"""
|
| 265 |
+
Return True if the input numpy array is empy. Check if
|
| 266 |
+
- all values are all the same value (0, 1 or given 'val' input float value)
|
| 267 |
+
- all values that are not NaN are a given 'val' float value
|
| 268 |
+
|
| 269 |
+
Args:
|
| 270 |
+
arr: input numpy array
|
| 271 |
+
val: value to use for check if array is empty
|
| 272 |
+
|
| 273 |
+
Returns:
|
| 274 |
+
bool: True if the input numpy array is empty, False otherwise
|
| 275 |
+
|
| 276 |
+
"""
|
| 277 |
+
|
| 278 |
+
arr_check5_tmp = np.copy(arr)
|
| 279 |
+
arr_size = arr.shape[0]
|
| 280 |
+
arr_check3 = np.ones((arr_size, arr_size))
|
| 281 |
+
check1 = np.array_equal(arr, arr_check3)
|
| 282 |
+
check2 = np.array_equal(arr, np.zeros((arr_size, arr_size)))
|
| 283 |
+
arr_check3 *= val
|
| 284 |
+
check3 = np.array_equal(arr, arr_check3)
|
| 285 |
+
arr[np.isnan(arr)] = 0
|
| 286 |
+
check4 = np.array_equal(arr, np.zeros((arr_size, arr_size)))
|
| 287 |
+
arr_check5 = np.ones((arr_size, arr_size)) * val
|
| 288 |
+
arr_check5_tmp[np.isnan(arr_check5_tmp)] = val
|
| 289 |
+
check5 = np.array_equal(arr_check5_tmp, arr_check5)
|
| 290 |
+
app_logger.debug(f"array checks:{check1}, {check2}, {check3}, {check4}, {check5}.")
|
| 291 |
+
return check1 or check2 or check3 or check4 or check5
|
samgis/io/{lambda_helpers.py → wrappers_helpers.py}
RENAMED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
"""lambda helper functions"""
|
| 2 |
from typing import Dict
|
| 3 |
-
from xyzservices import providers
|
| 4 |
|
| 5 |
from samgis import app_logger
|
| 6 |
from samgis.io.coordinates_pixel_conversion import get_latlng_to_pixel_coordinates
|
| 7 |
-
from samgis.utilities.constants import CUSTOM_RESPONSE_MESSAGES
|
| 8 |
-
from samgis.utilities.type_hints import ApiRequestBody, ContentTypes
|
| 9 |
from samgis.utilities.utilities import base64_decode
|
| 10 |
|
| 11 |
|
|
@@ -152,9 +152,30 @@ def get_parsed_request_body(event: Dict or str) -> ApiRequestBody:
|
|
| 152 |
return parsed_body
|
| 153 |
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
def get_url_tile(source_type: str):
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
-
if source_type.lower() == DEFAULT_TMS_NAME_SHORT:
|
| 159 |
-
return providers.query_name(DEFAULT_TMS_NAME)
|
| 160 |
return providers.query_name(source_type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""lambda helper functions"""
|
| 2 |
from typing import Dict
|
| 3 |
+
from xyzservices import providers, TileProvider
|
| 4 |
|
| 5 |
from samgis import app_logger
|
| 6 |
from samgis.io.coordinates_pixel_conversion import get_latlng_to_pixel_coordinates
|
| 7 |
+
from samgis.utilities.constants import COMPLETE_URL_TILES_MAPBOX, COMPLETE_URL_TILES_NEXTZEN, CUSTOM_RESPONSE_MESSAGES
|
| 8 |
+
from samgis.utilities.type_hints import ApiRequestBody, ContentTypes, TmsTerrainProvidersNames, TmsDefaultProvidersNames
|
| 9 |
from samgis.utilities.utilities import base64_decode
|
| 10 |
|
| 11 |
|
|
|
|
| 152 |
return parsed_body
|
| 153 |
|
| 154 |
|
| 155 |
+
mapbox_terrain_rgb = TileProvider(
|
| 156 |
+
name=TmsTerrainProvidersNames.MAPBOX_TERRAIN_TILES_NAME,
|
| 157 |
+
url=COMPLETE_URL_TILES_MAPBOX,
|
| 158 |
+
attribution=""
|
| 159 |
+
)
|
| 160 |
+
nextzen_terrain_rgb = TileProvider(
|
| 161 |
+
name=TmsTerrainProvidersNames.NEXTZEN_TERRAIN_TILES_NAME,
|
| 162 |
+
url=COMPLETE_URL_TILES_NEXTZEN,
|
| 163 |
+
attribution=""
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
def get_url_tile(source_type: str):
|
| 168 |
+
match source_type.lower():
|
| 169 |
+
case TmsDefaultProvidersNames.DEFAULT_TILES_NAME_SHORT:
|
| 170 |
+
return providers.query_name(TmsDefaultProvidersNames.DEFAULT_TILES_NAME_SHORT)
|
| 171 |
+
case TmsTerrainProvidersNames.MAPBOX_TERRAIN_TILES_NAME:
|
| 172 |
+
return mapbox_terrain_rgb
|
| 173 |
+
case TmsTerrainProvidersNames.NEXTZEN_TERRAIN_TILES_NAME:
|
| 174 |
+
app_logger.info("nextzen_terrain_rgb:", nextzen_terrain_rgb)
|
| 175 |
+
return nextzen_terrain_rgb
|
| 176 |
|
|
|
|
|
|
|
| 177 |
return providers.query_name(source_type)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def check_source_type_is_terrain(source: str | TileProvider):
|
| 181 |
+
return isinstance(source, TileProvider) and source.name in list(TmsTerrainProvidersNames)
|
samgis/prediction_api/predictors.py
CHANGED
|
@@ -3,10 +3,14 @@ from numpy import array as np_array, uint8, zeros, ndarray
|
|
| 3 |
|
| 4 |
from samgis import app_logger, MODEL_FOLDER
|
| 5 |
from samgis.io.geo_helpers import get_vectorized_raster_as_geojson
|
|
|
|
| 6 |
from samgis.io.tms2geotiff import download_extent
|
|
|
|
| 7 |
from samgis.prediction_api.sam_onnx import SegmentAnythingONNX
|
| 8 |
-
from samgis.utilities.constants import MODEL_ENCODER_NAME, MODEL_DECODER_NAME,
|
| 9 |
-
|
|
|
|
|
|
|
| 10 |
|
| 11 |
models_dict = {"fastsam": {"instance": None}}
|
| 12 |
|
|
@@ -16,7 +20,7 @@ def samexporter_predict(
|
|
| 16 |
prompt: list_dict,
|
| 17 |
zoom: float,
|
| 18 |
model_name: str = "fastsam",
|
| 19 |
-
source: str =
|
| 20 |
) -> dict_str_int:
|
| 21 |
"""
|
| 22 |
Return predictions as a geojson from a geo-referenced image using the given input prompt.
|
|
@@ -49,6 +53,14 @@ def samexporter_predict(
|
|
| 49 |
pt0, pt1 = bbox
|
| 50 |
app_logger.info(f"tile_source: {source}: downloading geo-referenced raster with bbox {bbox}, zoom {zoom}.")
|
| 51 |
img, transform = download_extent(w=pt1[1], s=pt1[0], e=pt0[1], n=pt0[0], zoom=zoom, source=source)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
app_logger.info(
|
| 53 |
f"img type {type(img)} with shape/size:{img.size}, transform type: {type(transform)}, transform:{transform}.")
|
| 54 |
|
|
|
|
| 3 |
|
| 4 |
from samgis import app_logger, MODEL_FOLDER
|
| 5 |
from samgis.io.geo_helpers import get_vectorized_raster_as_geojson
|
| 6 |
+
from samgis.io.raster_helpers import get_raster_terrain_rgb_like, get_rgb_prediction_image
|
| 7 |
from samgis.io.tms2geotiff import download_extent
|
| 8 |
+
from samgis.io.wrappers_helpers import check_source_type_is_terrain
|
| 9 |
from samgis.prediction_api.sam_onnx import SegmentAnythingONNX
|
| 10 |
+
from samgis.utilities.constants import MODEL_ENCODER_NAME, MODEL_DECODER_NAME, DEFAULT_URL_TILES, SLOPE_CELLSIZE, \
|
| 11 |
+
DEFAULT_INPUT_SHAPE
|
| 12 |
+
from samgis.utilities.type_hints import llist_float, dict_str_int, list_dict, tuple_ndarr_int, PIL_Image, \
|
| 13 |
+
TmsTerrainProvidersNames
|
| 14 |
|
| 15 |
models_dict = {"fastsam": {"instance": None}}
|
| 16 |
|
|
|
|
| 20 |
prompt: list_dict,
|
| 21 |
zoom: float,
|
| 22 |
model_name: str = "fastsam",
|
| 23 |
+
source: str = DEFAULT_URL_TILES
|
| 24 |
) -> dict_str_int:
|
| 25 |
"""
|
| 26 |
Return predictions as a geojson from a geo-referenced image using the given input prompt.
|
|
|
|
| 53 |
pt0, pt1 = bbox
|
| 54 |
app_logger.info(f"tile_source: {source}: downloading geo-referenced raster with bbox {bbox}, zoom {zoom}.")
|
| 55 |
img, transform = download_extent(w=pt1[1], s=pt1[0], e=pt0[1], n=pt0[0], zoom=zoom, source=source)
|
| 56 |
+
if check_source_type_is_terrain(source):
|
| 57 |
+
app_logger.info(f"terrain-rgb like raster: transforms it into a DEM")
|
| 58 |
+
dem = get_raster_terrain_rgb_like(img, source.name)
|
| 59 |
+
# set a slope cell size proportional to the image width
|
| 60 |
+
slope_cellsize = int(img.shape[1] * SLOPE_CELLSIZE / DEFAULT_INPUT_SHAPE[1])
|
| 61 |
+
app_logger.info(f"terrain-rgb like raster: compute slope, curvature using {slope_cellsize} as cell size.")
|
| 62 |
+
img = get_rgb_prediction_image(dem, slope_cellsize)
|
| 63 |
+
|
| 64 |
app_logger.info(
|
| 65 |
f"img type {type(img)} with shape/size:{img.size}, transform type: {type(transform)}, transform:{transform}.")
|
| 66 |
|
samgis/prediction_api/sam_onnx.py
CHANGED
|
@@ -29,14 +29,15 @@ from cv2 import INTER_LINEAR, warpAffine
|
|
| 29 |
from onnxruntime import get_available_providers, InferenceSession
|
| 30 |
|
| 31 |
from samgis import app_logger
|
|
|
|
| 32 |
|
| 33 |
|
| 34 |
class SegmentAnythingONNX:
|
| 35 |
"""Segmentation model using SegmentAnything"""
|
| 36 |
|
| 37 |
def __init__(self, encoder_model_path, decoder_model_path) -> None:
|
| 38 |
-
self.target_size =
|
| 39 |
-
self.input_size =
|
| 40 |
|
| 41 |
# Load models
|
| 42 |
providers = get_available_providers()
|
|
|
|
| 29 |
from onnxruntime import get_available_providers, InferenceSession
|
| 30 |
|
| 31 |
from samgis import app_logger
|
| 32 |
+
from samgis.utilities.constants import DEFAULT_INPUT_SHAPE
|
| 33 |
|
| 34 |
|
| 35 |
class SegmentAnythingONNX:
|
| 36 |
"""Segmentation model using SegmentAnything"""
|
| 37 |
|
| 38 |
def __init__(self, encoder_model_path, decoder_model_path) -> None:
|
| 39 |
+
self.target_size = DEFAULT_INPUT_SHAPE[1]
|
| 40 |
+
self.input_size = DEFAULT_INPUT_SHAPE
|
| 41 |
|
| 42 |
# Load models
|
| 43 |
providers = get_available_providers()
|
samgis/utilities/constants.py
CHANGED
|
@@ -13,9 +13,6 @@ MODEL_ENCODER_NAME = "mobile_sam.encoder.onnx"
|
|
| 13 |
MODEL_DECODER_NAME = "sam_vit_h_4b8939.decoder.onnx"
|
| 14 |
TILE_SIZE = 256
|
| 15 |
EARTH_EQUATORIAL_RADIUS = 6378137.0
|
| 16 |
-
DEFAULT_TMS_NAME_SHORT = "openstreetmap"
|
| 17 |
-
DEFAULT_TMS_NAME = "OpenStreetMap.Mapnik"
|
| 18 |
-
DEFAULT_TMS = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'
|
| 19 |
WKT_3857 = 'PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,'
|
| 20 |
WKT_3857 += 'AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],'
|
| 21 |
WKT_3857 += 'UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],'
|
|
@@ -33,3 +30,14 @@ N_WAIT = 0
|
|
| 33 |
N_MAX_RETRIES = 2
|
| 34 |
N_CONNECTION = 2
|
| 35 |
ZOOM_AUTO = "auto"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
MODEL_DECODER_NAME = "sam_vit_h_4b8939.decoder.onnx"
|
| 14 |
TILE_SIZE = 256
|
| 15 |
EARTH_EQUATORIAL_RADIUS = 6378137.0
|
|
|
|
|
|
|
|
|
|
| 16 |
WKT_3857 = 'PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,'
|
| 17 |
WKT_3857 += 'AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],'
|
| 18 |
WKT_3857 += 'UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],'
|
|
|
|
| 30 |
N_MAX_RETRIES = 2
|
| 31 |
N_CONNECTION = 2
|
| 32 |
ZOOM_AUTO = "auto"
|
| 33 |
+
DEFAULT_URL_TILES = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'
|
| 34 |
+
DOMAIN_URL_TILES_MAPBOX = "api.mapbox.com"
|
| 35 |
+
RELATIVE_URL_TILES_MAPBOX = "v/mapbox.terrain-rgb/{zoom}/{x}/{y}{@2x}.pngraw?access_token={TOKEN}"
|
| 36 |
+
COMPLETE_URL_TILES_MAPBOX = f"https://{DOMAIN_URL_TILES_MAPBOX}/{RELATIVE_URL_TILES_MAPBOX}"
|
| 37 |
+
# https://s3.amazonaws.com/elevation-tiles-prod/terrarium/13/1308/3167.png
|
| 38 |
+
DOMAIN_URL_TILES_NEXTZEN = "s3.amazonaws.com"
|
| 39 |
+
RELATIVE_URL_TILES_NEXTZEN = "elevation-tiles-prod/terrarium/{z}/{x}/{y}.png" # "terrarium/{z}/{x}/{y}.png"
|
| 40 |
+
COMPLETE_URL_TILES_NEXTZEN = f"https://{DOMAIN_URL_TILES_NEXTZEN}/{RELATIVE_URL_TILES_NEXTZEN}"
|
| 41 |
+
CHANNEL_EXAGGERATIONS_LIST = [2.5, 1.1, 2.0]
|
| 42 |
+
DEFAULT_INPUT_SHAPE = 684, 1024
|
| 43 |
+
SLOPE_CELLSIZE = 61
|
samgis/utilities/type_hints.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""custom type hints"""
|
| 2 |
-
from enum import IntEnum, Enum
|
| 3 |
from typing import TypedDict
|
| 4 |
|
| 5 |
from PIL.Image import Image
|
|
@@ -7,8 +7,6 @@ from affine import Affine
|
|
| 7 |
from numpy import ndarray
|
| 8 |
from pydantic import BaseModel
|
| 9 |
|
| 10 |
-
from samgis.utilities.constants import DEFAULT_TMS
|
| 11 |
-
|
| 12 |
|
| 13 |
dict_str_int = dict[str, int]
|
| 14 |
dict_str = dict[str]
|
|
@@ -25,6 +23,18 @@ PIL_Image = Image
|
|
| 25 |
tuple_ndarray_transform = tuple[ndarray, Affine]
|
| 26 |
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
class LatLngDict(BaseModel):
|
| 29 |
"""Generic geographic latitude-longitude type"""
|
| 30 |
lat: float
|
|
|
|
| 1 |
"""custom type hints"""
|
| 2 |
+
from enum import IntEnum, Enum, StrEnum
|
| 3 |
from typing import TypedDict
|
| 4 |
|
| 5 |
from PIL.Image import Image
|
|
|
|
| 7 |
from numpy import ndarray
|
| 8 |
from pydantic import BaseModel
|
| 9 |
|
|
|
|
|
|
|
| 10 |
|
| 11 |
dict_str_int = dict[str, int]
|
| 12 |
dict_str = dict[str]
|
|
|
|
| 23 |
tuple_ndarray_transform = tuple[ndarray, Affine]
|
| 24 |
|
| 25 |
|
| 26 |
+
class TmsDefaultProvidersNames(StrEnum):
|
| 27 |
+
"""Default xyz provider names"""
|
| 28 |
+
DEFAULT_TILES_NAME_SHORT = "openstreetmap"
|
| 29 |
+
DEFAULT_TILES_NAME = "openstreetmap.mapnik"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class TmsTerrainProvidersNames(StrEnum):
|
| 33 |
+
"""Custom xyz provider names for digital elevation models"""
|
| 34 |
+
MAPBOX_TERRAIN_TILES_NAME = "mapbox.terrain-rgb"
|
| 35 |
+
NEXTZEN_TERRAIN_TILES_NAME = "nextzen.terrarium"
|
| 36 |
+
|
| 37 |
+
|
| 38 |
class LatLngDict(BaseModel):
|
| 39 |
"""Generic geographic latitude-longitude type"""
|
| 40 |
lat: float
|
wrappers/fastapi_wrapper.py
CHANGED
|
@@ -7,7 +7,7 @@ from fastapi.responses import FileResponse, JSONResponse
|
|
| 7 |
from fastapi.staticfiles import StaticFiles
|
| 8 |
|
| 9 |
from samgis import app_logger
|
| 10 |
-
from samgis.io.
|
| 11 |
from samgis.utilities.type_hints import ApiRequestBody
|
| 12 |
|
| 13 |
app = FastAPI()
|
|
|
|
| 7 |
from fastapi.staticfiles import StaticFiles
|
| 8 |
|
| 9 |
from samgis import app_logger
|
| 10 |
+
from samgis.io.wrappers_helpers import get_parsed_bbox_points
|
| 11 |
from samgis.utilities.type_hints import ApiRequestBody
|
| 12 |
|
| 13 |
app = FastAPI()
|
wrappers/lambda_wrapper.py
CHANGED
|
@@ -6,7 +6,7 @@ from aws_lambda_powertools.utilities.typing import LambdaContext
|
|
| 6 |
from pydantic import ValidationError
|
| 7 |
|
| 8 |
from samgis import app_logger
|
| 9 |
-
from samgis.io.
|
| 10 |
from samgis.prediction_api.predictors import samexporter_predict
|
| 11 |
|
| 12 |
|
|
|
|
| 6 |
from pydantic import ValidationError
|
| 7 |
|
| 8 |
from samgis import app_logger
|
| 9 |
+
from samgis.io.wrappers_helpers import get_parsed_request_body, get_parsed_bbox_points, get_response
|
| 10 |
from samgis.prediction_api.predictors import samexporter_predict
|
| 11 |
|
| 12 |
|