fimbench 0.1.0__py3-none-any.whl
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.
- fimbench/__init__.py +66 -0
- fimbench/_log.py +15 -0
- fimbench/processing_floodmap/README.md +41 -0
- fimbench/processing_floodmap/__init__.py +23 -0
- fimbench/processing_floodmap/fema_ble.py +398 -0
- fimbench/processing_floodmap/hwm.py +447 -0
- fimbench/processing_floodmap/tier1.py +507 -0
- fimbench/processing_floodmap/tier2.py +472 -0
- fimbench/processing_floodmap/tier3.py +463 -0
- fimbench/processing_floodmap/utils.py +114 -0
- fimbench/publish/README.md +92 -0
- fimbench/publish/__init__.py +26 -0
- fimbench/publish/arcgis_online.py +323 -0
- fimbench/publish/s3/__init__.py +10 -0
- fimbench/publish/s3/s3_client.py +98 -0
- fimbench/publish/s3/s3_io.py +19 -0
- fimbench/publish/upload_benchmarkfloodmap.py +68 -0
- fimbench/publish/upload_catalogntilesintos3.py +16 -0
- fimbench/query/README.md +104 -0
- fimbench/query/__init__.py +15 -0
- fimbench/query/access_benchfim.py +899 -0
- fimbench/query/utilis.py +720 -0
- fimbench/webcontent_utils/README.md +83 -0
- fimbench/webcontent_utils/__init__.py +17 -0
- fimbench/webcontent_utils/build_catalog.py +346 -0
- fimbench/webcontent_utils/smoothen_fimextent.py +117 -0
- fimbench/webcontent_utils/tiling.py +587 -0
- fimbench/webcontent_utils/viewtile_locally/README.md +74 -0
- fimbench/webcontent_utils/viewtile_locally/__init__.py +9 -0
- fimbench/webcontent_utils/viewtile_locally/serve_tiles.py +49 -0
- fimbench/webcontent_utils/viewtile_locally/view.html +85 -0
- fimbench-0.1.0.dist-info/METADATA +256 -0
- fimbench-0.1.0.dist-info/RECORD +36 -0
- fimbench-0.1.0.dist-info/WHEEL +5 -0
- fimbench-0.1.0.dist-info/licenses/LICENSE +61 -0
- fimbench-0.1.0.dist-info/top_level.txt +1 -0
fimbench/__init__.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fimbench
|
|
3
|
+
|
|
4
|
+
FIM benchmarking utilities. Pipeline: processing_floodmap (standardize) ->
|
|
5
|
+
webcontent_utils (catalog + tiles) -> query (availability) -> publish (S3 /
|
|
6
|
+
ArcGIS Online). Public classes are re-exported at the top level
|
|
7
|
+
(e.g. fimbench.Tier1Processor) and resolved lazily so `import fimbench` stays
|
|
8
|
+
light.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
|
|
15
|
+
# Lazy top-level names (see __getattr__); avoids importing rasterio/geopandas/
|
|
16
|
+
# arcgis until a name is actually used.
|
|
17
|
+
__all__: list[str] = [
|
|
18
|
+
# subpackages
|
|
19
|
+
"processing_floodmap",
|
|
20
|
+
"webcontent_utils",
|
|
21
|
+
"query",
|
|
22
|
+
"publish",
|
|
23
|
+
# processing_floodmap classes -> fimbench.Tier1Processor, etc.
|
|
24
|
+
"Tier1Processor",
|
|
25
|
+
"Tier2Processor",
|
|
26
|
+
"Tier3Processor",
|
|
27
|
+
"FemaBleProcessor",
|
|
28
|
+
"HwmProcessor",
|
|
29
|
+
# webcontent_utils classes
|
|
30
|
+
"FIMCatalogBuilder",
|
|
31
|
+
"CatalogandTileManager",
|
|
32
|
+
# query
|
|
33
|
+
"benchFIMquery",
|
|
34
|
+
# publish
|
|
35
|
+
"PublishFIMExtent2ArcGISOnline",
|
|
36
|
+
"upload_benchmarkfloodmap",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
# name -> (submodule, attribute) for lazy top-level re-export.
|
|
40
|
+
_LAZY = {
|
|
41
|
+
"Tier1Processor": ("processing_floodmap", "Tier1Processor"),
|
|
42
|
+
"Tier2Processor": ("processing_floodmap", "Tier2Processor"),
|
|
43
|
+
"Tier3Processor": ("processing_floodmap", "Tier3Processor"),
|
|
44
|
+
"FemaBleProcessor": ("processing_floodmap", "FemaBleProcessor"),
|
|
45
|
+
"HwmProcessor": ("processing_floodmap", "HwmProcessor"),
|
|
46
|
+
"FIMCatalogBuilder": ("webcontent_utils", "FIMCatalogBuilder"),
|
|
47
|
+
"CatalogandTileManager": ("webcontent_utils", "CatalogandTileManager"),
|
|
48
|
+
"benchFIMquery": ("query", "benchFIMquery"),
|
|
49
|
+
"PublishFIMExtent2ArcGISOnline": ("publish", "PublishFIMExtent2ArcGISOnline"),
|
|
50
|
+
"upload_benchmarkfloodmap": ("publish", "upload_benchmarkfloodmap"),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def __getattr__(name: str):
|
|
55
|
+
import importlib
|
|
56
|
+
|
|
57
|
+
target = _LAZY.get(name)
|
|
58
|
+
if target is not None:
|
|
59
|
+
submod, attr = target
|
|
60
|
+
module = importlib.import_module(f"{__name__}.{submod}")
|
|
61
|
+
return getattr(module, attr)
|
|
62
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def __dir__():
|
|
66
|
+
return sorted(__all__)
|
fimbench/_log.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fimbench._log
|
|
3
|
+
|
|
4
|
+
Shared status logger so every subpackage emits output the same way:
|
|
5
|
+
[HH:MM:SS] message
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def log(msg):
|
|
14
|
+
"""Print a timestamped status line."""
|
|
15
|
+
print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# fimbench.processing_floodmap
|
|
2
|
+
|
|
3
|
+
This standalone module helps to standardize a raw benchmark flood map into the FIMbench database format. It is now structured as one class per flooding map source;
|
|
4
|
+
each writes the renamed GeoTIFF, metadata.json, and AOI.gpkg into a per-map
|
|
5
|
+
folder under the destination. Logic is kept faithful to the source notebooks,
|
|
6
|
+
so the output matches.
|
|
7
|
+
|
|
8
|
+
Intersected HUC8 watersheds informations for metadata within US are resolved on the fly from the ArcGIS REST service
|
|
9
|
+
(`utils.get_intersected_huc8`)
|
|
10
|
+
|
|
11
|
+
## Classes
|
|
12
|
+
|
|
13
|
+
- `Tier1Processor` — Aerial Imagery (AI)
|
|
14
|
+
- `Tier2Processor` — Planet Scope Scene (PSS)
|
|
15
|
+
- `Tier3Processor` — Sentinel 1A (S1A)
|
|
16
|
+
- `FemaBleProcessor` — FEMA Base Level Engineering (BLE), uses a return-period event
|
|
17
|
+
- `HwmProcessor` — High Water Mark (HWM), single TIF + start/end date
|
|
18
|
+
|
|
19
|
+
Defaults (`SENSOR_CODE`, `SOURCE`, `NODATA_VAL`, `SIMPLIFY_TOL`, ...) are class
|
|
20
|
+
attributes and can be overridden via keyword args.
|
|
21
|
+
|
|
22
|
+
## Use
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from fimbench.processing_floodmap import Tier1Processor
|
|
26
|
+
|
|
27
|
+
Tier1Processor().process("in/rasters", "out/")
|
|
28
|
+
# override a default:
|
|
29
|
+
Tier1Processor(source="My source").process("in/rasters", "out/")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Each module also exposes a `process(...)` shortcut:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from fimbench.processing_floodmap import tier1, fema_ble, hwm
|
|
36
|
+
|
|
37
|
+
tier1.process("in/", "out/")
|
|
38
|
+
fema_ble.process("in/", "out/", event="100")
|
|
39
|
+
hwm.process("map.tif", "out/", start_date="160928", end_date="161009")
|
|
40
|
+
```
|
|
41
|
+
**For more usage notes refer to the [tests](../../../tests/) or [docs](../../../docs/) for the fimbench python package**
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from . import tier1, tier2, tier3, fema_ble, hwm, utils
|
|
5
|
+
from .tier1 import Tier1Processor
|
|
6
|
+
from .tier2 import Tier2Processor
|
|
7
|
+
from .tier3 import Tier3Processor
|
|
8
|
+
from .fema_ble import FemaBleProcessor
|
|
9
|
+
from .hwm import HwmProcessor
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Tier1Processor",
|
|
13
|
+
"Tier2Processor",
|
|
14
|
+
"Tier3Processor",
|
|
15
|
+
"FemaBleProcessor",
|
|
16
|
+
"HwmProcessor",
|
|
17
|
+
"tier1",
|
|
18
|
+
"tier2",
|
|
19
|
+
"tier3",
|
|
20
|
+
"fema_ble",
|
|
21
|
+
"hwm",
|
|
22
|
+
"utils",
|
|
23
|
+
]
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authors: Supath Dhital (sdhital@ua.edu), Dipsikha Devi (ddevi@ua.edu)
|
|
3
|
+
Updated: June 2026
|
|
4
|
+
|
|
5
|
+
FEMA Base Level Engineering (BLE) flood map processing.
|
|
6
|
+
|
|
7
|
+
Standardizes FEMA BLE synthetic (return-period) flood rasters into the FIM
|
|
8
|
+
database format: renamed GeoTIFF, metadata.json, and AOI.gpkg per flood map.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import shutil
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import numpy as np
|
|
16
|
+
import rasterio
|
|
17
|
+
from rasterio.features import shapes
|
|
18
|
+
from rasterio.warp import calculate_default_transform, reproject, Resampling
|
|
19
|
+
from shapely.geometry import shape
|
|
20
|
+
from shapely.ops import unary_union
|
|
21
|
+
from shapely.validation import make_valid
|
|
22
|
+
import geopandas as gpd
|
|
23
|
+
|
|
24
|
+
from .._log import log as _log
|
|
25
|
+
from .utils import get_intersected_huc8, list_input_tifs
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FemaBleProcessor:
|
|
29
|
+
"""Standardize FEMA Base Level Engineering flood maps for the FIM database."""
|
|
30
|
+
|
|
31
|
+
SENSOR_CODE = "BLE"
|
|
32
|
+
Full_Form = "Base Level Engineering"
|
|
33
|
+
SOURCE = "NOAA/NWS Office of Water Prediction (OWP)"
|
|
34
|
+
|
|
35
|
+
NODATA_VAL = -9999
|
|
36
|
+
BLOCK_SIZE = 512
|
|
37
|
+
SIMPLIFY_TOL = 0.0001 # ~11 m at the equator
|
|
38
|
+
|
|
39
|
+
def __init__(self, *, sensor_code=None, full_form=None, source=None,
|
|
40
|
+
nodata_val=None, block_size=None, simplify_tol=None):
|
|
41
|
+
# Class attributes are the defaults.
|
|
42
|
+
if sensor_code is not None:
|
|
43
|
+
self.SENSOR_CODE = sensor_code
|
|
44
|
+
if full_form is not None:
|
|
45
|
+
self.Full_Form = full_form
|
|
46
|
+
if source is not None:
|
|
47
|
+
self.SOURCE = source
|
|
48
|
+
if nodata_val is not None:
|
|
49
|
+
self.NODATA_VAL = nodata_val
|
|
50
|
+
if block_size is not None:
|
|
51
|
+
self.BLOCK_SIZE = block_size
|
|
52
|
+
if simplify_tol is not None:
|
|
53
|
+
self.SIMPLIFY_TOL = simplify_tol
|
|
54
|
+
|
|
55
|
+
# Folder for intermediate rasters; set per run in process().
|
|
56
|
+
self._work_dir = None
|
|
57
|
+
|
|
58
|
+
def log(self, msg):
|
|
59
|
+
_log(msg)
|
|
60
|
+
|
|
61
|
+
def _work_path(self, name):
|
|
62
|
+
# Place an intermediate file in the work dir (else next to the input).
|
|
63
|
+
if self._work_dir is not None:
|
|
64
|
+
return Path(self._work_dir) / name
|
|
65
|
+
return Path(name)
|
|
66
|
+
|
|
67
|
+
# Assign nodata, datatype and tile size
|
|
68
|
+
def process_tif(self, input_tif, nodata_val=None, block_size=None):
|
|
69
|
+
nodata_val = self.NODATA_VAL if nodata_val is None else nodata_val
|
|
70
|
+
block_size = self.BLOCK_SIZE if block_size is None else block_size
|
|
71
|
+
input_tif = Path(input_tif)
|
|
72
|
+
output_tif = self._work_path(input_tif.stem + "_nodata.tif")
|
|
73
|
+
output_tif.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
|
|
75
|
+
with rasterio.open(input_tif) as src:
|
|
76
|
+
m = src.read(masked=True)
|
|
77
|
+
src_dtype = np.dtype(src.dtypes[0])
|
|
78
|
+
if np.issubdtype(src_dtype, np.floating):
|
|
79
|
+
out_dtype = "float32"
|
|
80
|
+
else:
|
|
81
|
+
out_dtype = "int32"
|
|
82
|
+
|
|
83
|
+
out = np.full(m.shape, nodata_val, dtype=out_dtype)
|
|
84
|
+
valid = ~m.mask
|
|
85
|
+
out[valid] = m.data[valid].astype(out_dtype, copy=False)
|
|
86
|
+
|
|
87
|
+
profile = src.profile.copy()
|
|
88
|
+
profile.update(
|
|
89
|
+
dtype=out_dtype,
|
|
90
|
+
nodata=nodata_val,
|
|
91
|
+
tiled=True,
|
|
92
|
+
blockxsize=block_size,
|
|
93
|
+
blockysize=block_size,
|
|
94
|
+
compress="lzw"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
with rasterio.open(output_tif, "w", **profile) as dst:
|
|
98
|
+
dst.write(out)
|
|
99
|
+
try:
|
|
100
|
+
dst.update_tags(**src.tags())
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
return output_tif
|
|
105
|
+
|
|
106
|
+
# Reproject to the target coordinate system (default EPSG:5070)
|
|
107
|
+
def reproject_raster(self, input_file, target_epsg):
|
|
108
|
+
input_file = Path(input_file)
|
|
109
|
+
output_file = self._work_path(input_file.stem + f"_reprojected_{target_epsg}.tif")
|
|
110
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
|
|
112
|
+
with rasterio.open(input_file) as src:
|
|
113
|
+
transform, width, height = calculate_default_transform(
|
|
114
|
+
src.crs, f"EPSG:{target_epsg}", src.width, src.height, *src.bounds)
|
|
115
|
+
|
|
116
|
+
metadata = src.meta.copy()
|
|
117
|
+
metadata.update({
|
|
118
|
+
"crs": f"EPSG:{target_epsg}",
|
|
119
|
+
"transform": transform,
|
|
120
|
+
"width": width,
|
|
121
|
+
"height": height,
|
|
122
|
+
"compress": "lzw"
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
with rasterio.open(output_file, "w", **metadata) as dst:
|
|
126
|
+
for i in range(1, src.count + 1):
|
|
127
|
+
reproject(
|
|
128
|
+
source=rasterio.band(src, i),
|
|
129
|
+
destination=rasterio.band(dst, i),
|
|
130
|
+
src_transform=src.transform,
|
|
131
|
+
src_crs=src.crs,
|
|
132
|
+
dst_transform=transform,
|
|
133
|
+
dst_crs=f"EPSG:{target_epsg}",
|
|
134
|
+
resampling=Resampling.nearest
|
|
135
|
+
)
|
|
136
|
+
return output_file
|
|
137
|
+
|
|
138
|
+
# Extract the resolution from the projected file
|
|
139
|
+
def get_resolution_string(self, geo_tif_5070):
|
|
140
|
+
with rasterio.open(geo_tif_5070) as src:
|
|
141
|
+
res_x, res_y = src.res
|
|
142
|
+
avg_res = (res_x + res_y) / 2.0
|
|
143
|
+
rounded_res = round(avg_res, 1)
|
|
144
|
+
res_str = f"{str(rounded_res).replace('.', '_')}m" # e.g., 10.0 -> "10_0m"
|
|
145
|
+
res_txt = f"{rounded_res:.2f} m"
|
|
146
|
+
return rounded_res, res_str, res_txt
|
|
147
|
+
|
|
148
|
+
def get_raster_centroid(self, out_4326):
|
|
149
|
+
with rasterio.open(out_4326) as src:
|
|
150
|
+
b = src.bounds
|
|
151
|
+
centroid_x = (b.left + b.right) / 2
|
|
152
|
+
centroid_y = (b.top + b.bottom) / 2
|
|
153
|
+
crs = src.crs
|
|
154
|
+
|
|
155
|
+
return centroid_x, centroid_y, crs, b
|
|
156
|
+
|
|
157
|
+
# Decimal degrees to DMS, e.g. 95 31'44" W and 31 04'36" N -> 953144W310436N
|
|
158
|
+
def decimal_to_dms_str(self, dec, is_lat=True):
|
|
159
|
+
direction = ''
|
|
160
|
+
if is_lat:
|
|
161
|
+
direction = 'N' if dec >= 0 else 'S'
|
|
162
|
+
else:
|
|
163
|
+
direction = 'E' if dec >= 0 else 'W'
|
|
164
|
+
|
|
165
|
+
dec = abs(dec)
|
|
166
|
+
degrees = int(dec)
|
|
167
|
+
minutes = int((dec - degrees) * 60)
|
|
168
|
+
seconds = int(((dec - degrees) * 60 - minutes) * 60)
|
|
169
|
+
|
|
170
|
+
return f"{degrees:02d}{minutes:02d}{seconds:02d}{direction}"
|
|
171
|
+
|
|
172
|
+
def build_metadata_dict(
|
|
173
|
+
self, src_4326, src_5070, new_filename, res_m_float, res_txt, dms_code, x, y, state_list, huc8_list, name_list, EVENT
|
|
174
|
+
):
|
|
175
|
+
with rasterio.open(src_4326) as src:
|
|
176
|
+
meta = {
|
|
177
|
+
"File_Name": new_filename,
|
|
178
|
+
"Full form of the sensor code": self.Full_Form,
|
|
179
|
+
"HUC8": huc8_list,
|
|
180
|
+
"Format": src.driver,
|
|
181
|
+
"Nodata_Value": src.nodata,
|
|
182
|
+
"Resolution in meter": res_m_float,
|
|
183
|
+
"Datatype": src.dtypes[0],
|
|
184
|
+
"Compression Type": src.tags(ns="IMAGE_STRUCTURE"),
|
|
185
|
+
"Extent": {
|
|
186
|
+
"xmin": src.bounds.left,
|
|
187
|
+
"ymin": src.bounds.bottom,
|
|
188
|
+
"xmax": src.bounds.right,
|
|
189
|
+
"ymax": src.bounds.top
|
|
190
|
+
},
|
|
191
|
+
"DMS_Code_centroid": dms_code,
|
|
192
|
+
"Projection": src.crs.to_string() if src.crs else "Unknown",
|
|
193
|
+
"Bands": src.count,
|
|
194
|
+
"Rows": src.height,
|
|
195
|
+
"Columns": src.width,
|
|
196
|
+
"State": f"{state_list}, USA" if state_list else "USA",
|
|
197
|
+
"Description": f"",
|
|
198
|
+
"River Basin Name": name_list,
|
|
199
|
+
"Source": self.SOURCE,
|
|
200
|
+
"Location of the centroid of the flood map": (x, y),
|
|
201
|
+
"Synthetic Flooding Event (return period (years))": EVENT,
|
|
202
|
+
"Keywords": ["flood", "hazard", "GIS"],
|
|
203
|
+
"Access_Rights": "Public",
|
|
204
|
+
"References": [
|
|
205
|
+
"1. Cohen, S., Baruah, A., Nikrou, P., Tian, D., & Liu, H. (2025). Toward robust evaluations of flood inundation predictions using remote sensing derived benchmark maps. Water Resources Research, 61(8), e2024WR039574.",
|
|
206
|
+
"2. Munasinghe, D., Cohen, S., Tian, D., Liu, H., Baruah, A., and Devi, D. (2025). A Database of Flood Maps using high-resolution Airborne Imagery and Machine Learning Models. In: CIROH Developers' Conference, May 28-30, 2025.",
|
|
207
|
+
"3. Devi, D., Dhital, S., Munasinghe, D., Cohen, S., Baruah, A., Chen, Y., ... & Pruitt, C. (2025). A Framework for the Evaluation of Flood Inundation Predictions Over Extensive Benchmark Databases.",
|
|
208
|
+
"4. Tian.D, Liu.H, Wang.L, Cohen.S, Thapa.P (2024).Enhancing satellite image-derived flood maps with hydrologically guided region growing method and high-resolution DEMs.Chapman Conference on Remote Sensing of the Water Cycle"
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
with rasterio.open(src_5070) as src_geom:
|
|
213
|
+
arr = src_geom.read(1, masked=True)
|
|
214
|
+
geom_crs = src_geom.crs
|
|
215
|
+
mask = arr > 0 # 1 is flood, 0 is dry
|
|
216
|
+
|
|
217
|
+
results = (
|
|
218
|
+
{'properties': {'raster_val': v}, 'geometry': s}
|
|
219
|
+
for i, (s, v) in enumerate(
|
|
220
|
+
shapes(arr, mask=mask, transform=src_geom.transform)
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
geoms = list(results)
|
|
225
|
+
if not geoms:
|
|
226
|
+
self.log("No flooded areas found in 5070 raster.")
|
|
227
|
+
return meta, None
|
|
228
|
+
|
|
229
|
+
gdf1 = gpd.GeoDataFrame.from_features(geoms)
|
|
230
|
+
gdf1.set_crs(geom_crs, inplace=True)
|
|
231
|
+
|
|
232
|
+
if self.SIMPLIFY_TOL:
|
|
233
|
+
gdf1['geometry'] = gdf1.simplify(self.SIMPLIFY_TOL, preserve_topology=True)
|
|
234
|
+
gdf1['geometry'] = gdf1.make_valid()
|
|
235
|
+
|
|
236
|
+
unified_geom = unary_union(gdf1.geometry)
|
|
237
|
+
|
|
238
|
+
return meta, unified_geom
|
|
239
|
+
|
|
240
|
+
def raster_to_polygon(self, in_raster, gpkg_path, metadata_row, valid_classes=(0, 1, 2), nodata=None, connectivity=8):
|
|
241
|
+
nodata = self.NODATA_VAL if nodata is None else nodata
|
|
242
|
+
in_raster = Path(in_raster)
|
|
243
|
+
with rasterio.open(in_raster) as src:
|
|
244
|
+
arr = src.read(1)
|
|
245
|
+
tfm = src.transform
|
|
246
|
+
crs = src.crs
|
|
247
|
+
mask = arr != nodata
|
|
248
|
+
polygons = []
|
|
249
|
+
for geom, val in shapes(arr, mask=mask, transform=tfm, connectivity=connectivity):
|
|
250
|
+
if val in valid_classes:
|
|
251
|
+
poly = shape(geom)
|
|
252
|
+
if not poly.is_valid:
|
|
253
|
+
poly = poly.buffer(0)
|
|
254
|
+
if not poly.is_empty:
|
|
255
|
+
polygons.append(poly)
|
|
256
|
+
|
|
257
|
+
if not polygons:
|
|
258
|
+
raise ValueError("No polygons found to build AOI.")
|
|
259
|
+
|
|
260
|
+
unified = unary_union(polygons)
|
|
261
|
+
gdf = gpd.GeoDataFrame([metadata_row], geometry=[unified], crs=crs)
|
|
262
|
+
gdf.to_file(gpkg_path, layer="AOI", driver="GPKG")
|
|
263
|
+
|
|
264
|
+
def raster_data_bbox(self, in_raster, gpkg_path, metadata_row, nodata=None):
|
|
265
|
+
nodata = self.NODATA_VAL if nodata is None else nodata
|
|
266
|
+
with rasterio.open(in_raster) as src:
|
|
267
|
+
arr = src.read(1)
|
|
268
|
+
crs = src.crs
|
|
269
|
+
valid = arr != nodata
|
|
270
|
+
if np.issubdtype(arr.dtype, np.floating):
|
|
271
|
+
valid &= ~np.isnan(arr)
|
|
272
|
+
valid_mask = valid.astype(np.uint8)
|
|
273
|
+
geoms = [
|
|
274
|
+
shape(geom)
|
|
275
|
+
for geom, _ in shapes(valid_mask, mask=valid_mask, transform=src.transform)
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
if not geoms:
|
|
279
|
+
raise ValueError("No valid data found to build AOI.")
|
|
280
|
+
|
|
281
|
+
unified = unary_union(geoms)
|
|
282
|
+
if self.SIMPLIFY_TOL:
|
|
283
|
+
unified = unified.simplify(self.SIMPLIFY_TOL, preserve_topology=True)
|
|
284
|
+
unified = make_valid(unified)
|
|
285
|
+
|
|
286
|
+
gdf = gpd.GeoDataFrame([metadata_row], geometry=[unified], crs=crs)
|
|
287
|
+
gdf.to_file(gpkg_path, layer="AOI", driver="GPKG")
|
|
288
|
+
|
|
289
|
+
def process(self, input_path, base_dest, event, intermediate_folder=None):
|
|
290
|
+
"""
|
|
291
|
+
Process a folder of rasters or a single .tif.
|
|
292
|
+
|
|
293
|
+
event: the synthetic return-period event (e.g. "100"), applied to all files.
|
|
294
|
+
intermediate_folder: where temporary rasters are written; defaults to a
|
|
295
|
+
fresh temp dir. Removed (best-effort) when processing finishes.
|
|
296
|
+
"""
|
|
297
|
+
BASE_DEST = Path(base_dest)
|
|
298
|
+
EVENT = event
|
|
299
|
+
|
|
300
|
+
tif_list = list_input_tifs(input_path)
|
|
301
|
+
if not tif_list:
|
|
302
|
+
self.log(f"No .tif files found under {input_path}")
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
self.log(f"Found {len(tif_list)} .tif file(s) under {input_path}")
|
|
306
|
+
|
|
307
|
+
# Set up the intermediate work dir (temp if not provided).
|
|
308
|
+
if intermediate_folder is None:
|
|
309
|
+
self._work_dir = Path(tempfile.mkdtemp(prefix="fimbench_"))
|
|
310
|
+
else:
|
|
311
|
+
self._work_dir = Path(intermediate_folder)
|
|
312
|
+
self._work_dir.mkdir(parents=True, exist_ok=True)
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
for tif in tif_list:
|
|
316
|
+
try:
|
|
317
|
+
self.log(f"Processing: {tif}")
|
|
318
|
+
|
|
319
|
+
# Pre-processing steps
|
|
320
|
+
nodata_tif = self.process_tif(tif)
|
|
321
|
+
out_5070 = self.reproject_raster(nodata_tif, 5070)
|
|
322
|
+
out_4326 = self.reproject_raster(nodata_tif, 4326)
|
|
323
|
+
res_m_float, res_str, res_txt = self.get_resolution_string(out_5070)
|
|
324
|
+
|
|
325
|
+
# Centroid (4326) + DMS
|
|
326
|
+
x, y, raster_crs, b = self.get_raster_centroid(out_4326)
|
|
327
|
+
lon_str = self.decimal_to_dms_str(x, is_lat=False)
|
|
328
|
+
lat_str = self.decimal_to_dms_str(y, is_lat=True)
|
|
329
|
+
dms_code = lon_str + lat_str
|
|
330
|
+
|
|
331
|
+
# Metadata and geometry generation (return period comes from EVENT)
|
|
332
|
+
temp_filename = f"{self.SENSOR_CODE}_{res_str}_{EVENT}_{dms_code}_BM.tif"
|
|
333
|
+
|
|
334
|
+
meta_dict, unified_geom = self.build_metadata_dict(
|
|
335
|
+
out_4326, out_5070, temp_filename, res_m_float, res_txt, dms_code,
|
|
336
|
+
x, y, "", [], [], EVENT
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# HUC overlay via the ArcGIS REST service (unified_geom is in EPSG:5070)
|
|
340
|
+
huc8_list, name_list, state_list = get_intersected_huc8(unified_geom, 5070)
|
|
341
|
+
|
|
342
|
+
# Update the metadata dictionary with the actual HUC results
|
|
343
|
+
meta_dict["HUC8"] = huc8_list
|
|
344
|
+
meta_dict["River Basin Name"] = name_list
|
|
345
|
+
meta_dict["State"] = f"{state_list}, USA" if state_list else "USA"
|
|
346
|
+
meta_dict["Description"] = (
|
|
347
|
+
f"The Flood Inundation Map (FIM) corresponds to FEMA Base Level Engineering "
|
|
348
|
+
f"for the {EVENT} flood with a spatial resolution of {res_txt}. "
|
|
349
|
+
f"The corresponding HUC IDs are {huc8_list}"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# File management
|
|
353
|
+
folder_name = f"{self.SENSOR_CODE}_{EVENT}_{dms_code}"
|
|
354
|
+
destination_folder = BASE_DEST / folder_name
|
|
355
|
+
destination_folder.mkdir(parents=True, exist_ok=True)
|
|
356
|
+
|
|
357
|
+
new_filename = f"{self.SENSOR_CODE}_{res_str}_{EVENT}_{dms_code}_BM.tif"
|
|
358
|
+
new_path = destination_folder / new_filename
|
|
359
|
+
|
|
360
|
+
shutil.copy2(out_4326, new_path)
|
|
361
|
+
self.log(f"Saved GeoTIFF: {new_path.name}")
|
|
362
|
+
|
|
363
|
+
# Save JSON
|
|
364
|
+
metadata_filename = new_path.name.replace("BM.tif", "metadata.json")
|
|
365
|
+
metadata_path = new_path.parent / metadata_filename
|
|
366
|
+
with open(metadata_path, "w") as jf:
|
|
367
|
+
json.dump(meta_dict, jf, indent=4)
|
|
368
|
+
self.log(f"Wrote metadata: {metadata_path.name}")
|
|
369
|
+
|
|
370
|
+
# AOI.gpkg
|
|
371
|
+
gpkg_path = new_path.parent / new_path.name.replace("BM.tif", "AOI.gpkg")
|
|
372
|
+
self.raster_data_bbox(out_4326, gpkg_path, meta_dict, nodata=self.NODATA_VAL)
|
|
373
|
+
self.log(f"Wrote AOI: {gpkg_path.name}")
|
|
374
|
+
|
|
375
|
+
except Exception as e:
|
|
376
|
+
self.log(f"Skipped {tif.name} due to error: {e}")
|
|
377
|
+
import traceback
|
|
378
|
+
traceback.print_exc()
|
|
379
|
+
|
|
380
|
+
self.log("Done.")
|
|
381
|
+
finally:
|
|
382
|
+
# Remove the intermediate work dir so the input folder stays clean
|
|
383
|
+
# (best-effort; ignore if it cannot be removed).
|
|
384
|
+
shutil.rmtree(self._work_dir, ignore_errors=True)
|
|
385
|
+
self._work_dir = None
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def process(input_path, base_dest, event, intermediate_folder=None, **overrides):
|
|
389
|
+
"""
|
|
390
|
+
Convenience wrapper: run FemaBleProcessor over a folder or a single .tif.
|
|
391
|
+
|
|
392
|
+
event: synthetic return-period event (e.g. "100").
|
|
393
|
+
intermediate_folder: temp raster dir (defaults to a temp dir, removed after).
|
|
394
|
+
Config overrides (e.g. source=...) are passed to the processor.
|
|
395
|
+
"""
|
|
396
|
+
FemaBleProcessor(**overrides).process(
|
|
397
|
+
input_path, base_dest, event, intermediate_folder=intermediate_folder
|
|
398
|
+
)
|