kopia lustrzana https://github.com/OpenDroneMap/WebODM
136 wiersze
5.2 KiB
Python
136 wiersze
5.2 KiB
Python
def calc_volume(input_dem, pts=None, pts_epsg=None, geojson_polygon=None, decimals=4,
|
|
base_method="triangulate", custom_base_z=None):
|
|
try:
|
|
import os
|
|
import rasterio
|
|
import rasterio.mask
|
|
from osgeo import osr
|
|
from scipy.optimize import curve_fit
|
|
from scipy.interpolate import griddata
|
|
import numpy as np
|
|
import json
|
|
import warnings
|
|
|
|
osr.UseExceptions()
|
|
warnings.filterwarnings("ignore", module='scipy.optimize')
|
|
|
|
if not os.path.isfile(input_dem):
|
|
raise IOError(f"{input_dem} does not exist")
|
|
|
|
crs = None
|
|
with rasterio.open(input_dem) as d:
|
|
if d.crs is None:
|
|
raise ValueError(f"{input_dem} does not have a CRS")
|
|
crs = osr.SpatialReference()
|
|
crs.ImportFromEPSG(d.crs.to_epsg())
|
|
|
|
if pts is None and pts_epsg is None and geojson_polygon is not None:
|
|
# Read GeoJSON points
|
|
pts = read_polygon(geojson_polygon)
|
|
return calc_volume(input_dem, pts=pts, pts_epsg=4326, decimals=decimals, base_method=base_method, custom_base_z=custom_base_z)
|
|
|
|
# Convert to DEM crs
|
|
src_crs = osr.SpatialReference()
|
|
src_crs.ImportFromEPSG(pts_epsg)
|
|
transformer = osr.CoordinateTransformation(src_crs, crs)
|
|
|
|
dem_pts = [list(transformer.TransformPoint(p[1], p[0]))[:2] for p in pts]
|
|
|
|
# Some checks
|
|
if len(dem_pts) < 2:
|
|
raise ValueError("Insufficient points to form a polygon")
|
|
|
|
# Close loop if needed
|
|
if not np.array_equal(dem_pts[0], dem_pts[-1]):
|
|
dem_pts.append(dem_pts[0])
|
|
|
|
polygon = {"coordinates": [dem_pts], "type": "Polygon"}
|
|
dem_pts = np.array(dem_pts)
|
|
|
|
# Remove last point (loop close)
|
|
dem_pts = dem_pts[:-1]
|
|
|
|
with rasterio.open(input_dem) as d:
|
|
px_w = d.transform[0]
|
|
px_h = d.transform[4]
|
|
|
|
# Area of a pixel in square units
|
|
px_area = abs(px_w * px_h)
|
|
|
|
rast_dem, transform = rasterio.mask.mask(d, [polygon], crop=True, all_touched=True, indexes=1, nodata=np.nan)
|
|
h, w = rast_dem.shape
|
|
|
|
# X/Y coordinates in transform coordinates
|
|
ys, xs = np.array(rasterio.transform.rowcol(transform, dem_pts[:,0], dem_pts[:,1]))
|
|
|
|
if np.any(xs<0) or np.any(xs>=w) or np.any(ys<0) or np.any(ys>=h):
|
|
raise ValueError("Points are out of bounds")
|
|
|
|
zs = rast_dem[ys,xs]
|
|
|
|
if base_method == "plane":
|
|
# Create a grid for interpolation
|
|
x_grid, y_grid = np.meshgrid(np.linspace(0, w - 1, w), np.linspace(0, h - 1, h))
|
|
|
|
# Perform curve fitting
|
|
linear_func = lambda xy, m1, m2, b: m1 * xy[0] + m2 * xy[1] + b
|
|
params, covariance = curve_fit(linear_func, np.vstack((xs, ys)), zs)
|
|
|
|
base = linear_func((x_grid, y_grid), *params)
|
|
elif base_method == "triangulate":
|
|
# Create a grid for interpolation
|
|
x_grid, y_grid = np.meshgrid(np.linspace(0, w - 1, w), np.linspace(0, h - 1, h))
|
|
|
|
# Tessellate the input point set to N-D simplices, and interpolate linearly on each simplex.
|
|
base = griddata(np.column_stack((xs, ys)), zs, (x_grid, y_grid), method='linear')
|
|
elif base_method == "average":
|
|
base = np.full((h, w), np.mean(zs))
|
|
elif base_method == "custom":
|
|
if custom_base_z is None:
|
|
raise ValueError("Base method set to custom, but no custom base Z specified")
|
|
base = np.full((h, w), float(custom_base_z))
|
|
elif base_method == "highest":
|
|
base = np.full((h, w), np.max(zs))
|
|
elif base_method == "lowest":
|
|
base = np.full((h, w), np.min(zs))
|
|
else:
|
|
raise ValueError(f"Invalid base method {base_method}")
|
|
|
|
base[np.isnan(rast_dem)] = np.nan
|
|
|
|
# Calculate volume
|
|
diff = rast_dem - base
|
|
volume = np.nansum(diff) * px_area
|
|
|
|
# import matplotlib.pyplot as plt
|
|
# fig, ax = plt.subplots()
|
|
# ax.imshow(base)
|
|
# plt.scatter(xs, ys, c=zs, cmap='viridis', s=50, edgecolors='k')
|
|
# plt.colorbar(label='Z values')
|
|
# plt.title('Debug')
|
|
# plt.show()
|
|
|
|
return {'output': np.abs(np.round(volume, decimals=decimals))}
|
|
except Exception as e:
|
|
return {'error': str(e)}
|
|
|
|
def read_polygon(file):
|
|
with open(file, 'r', encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
|
|
if data.get('type') == "FeatureCollection":
|
|
features = data.get("features", [{}])
|
|
else:
|
|
features = [data]
|
|
|
|
for feature in features:
|
|
if not 'geometry' in feature:
|
|
continue
|
|
|
|
# Check if the feature geometry type is Polygon
|
|
if feature['geometry']['type'] == 'Polygon':
|
|
# Extract polygon coordinates
|
|
coordinates = feature['geometry']['coordinates'][0] # Assuming exterior ring
|
|
return coordinates
|
|
|
|
raise IOError("No polygons found in %s" % file) |