BERATools 0.2.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.
- beratools/__init__.py +9 -0
- beratools/core/__init__.py +0 -0
- beratools/core/algo_centerline.py +351 -0
- beratools/core/constants.py +86 -0
- beratools/core/dijkstra_algorithm.py +460 -0
- beratools/core/logger.py +85 -0
- beratools/core/tool_base.py +133 -0
- beratools/gui/__init__.py +15 -0
- beratools/gui/batch_processing_dlg.py +463 -0
- beratools/gui/beratools.json +2300 -0
- beratools/gui/bt_data.py +487 -0
- beratools/gui/bt_gui_main.py +691 -0
- beratools/gui/cli.py +18 -0
- beratools/gui/gui.json +8 -0
- beratools/gui/img/BERALogo.png +0 -0
- beratools/gui/img/closed.gif +0 -0
- beratools/gui/img/closed.png +0 -0
- beratools/gui/img/open.gif +0 -0
- beratools/gui/img/open.png +0 -0
- beratools/gui/img/tool.gif +0 -0
- beratools/gui/img/tool.png +0 -0
- beratools/gui/map_window.py +146 -0
- beratools/gui/tool_widgets.py +493 -0
- beratools/gui_tk/ASCII Banners.txt +248 -0
- beratools/gui_tk/__init__.py +20 -0
- beratools/gui_tk/beratools_main.py +515 -0
- beratools/gui_tk/bt_widgets.py +442 -0
- beratools/gui_tk/cli.py +18 -0
- beratools/gui_tk/gui.json +8 -0
- beratools/gui_tk/img/BERALogo.png +0 -0
- beratools/gui_tk/img/closed.gif +0 -0
- beratools/gui_tk/img/closed.png +0 -0
- beratools/gui_tk/img/open.gif +0 -0
- beratools/gui_tk/img/open.png +0 -0
- beratools/gui_tk/img/tool.gif +0 -0
- beratools/gui_tk/img/tool.png +0 -0
- beratools/gui_tk/main.py +14 -0
- beratools/gui_tk/map_window.py +144 -0
- beratools/gui_tk/runner.py +1481 -0
- beratools/gui_tk/tooltip.py +55 -0
- beratools/third_party/pyqtlet2/__init__.py +9 -0
- beratools/third_party/pyqtlet2/leaflet/__init__.py +26 -0
- beratools/third_party/pyqtlet2/leaflet/control/__init__.py +6 -0
- beratools/third_party/pyqtlet2/leaflet/control/control.py +59 -0
- beratools/third_party/pyqtlet2/leaflet/control/draw.py +52 -0
- beratools/third_party/pyqtlet2/leaflet/control/layers.py +20 -0
- beratools/third_party/pyqtlet2/leaflet/core/Parser.py +24 -0
- beratools/third_party/pyqtlet2/leaflet/core/__init__.py +2 -0
- beratools/third_party/pyqtlet2/leaflet/core/evented.py +180 -0
- beratools/third_party/pyqtlet2/leaflet/layer/__init__.py +5 -0
- beratools/third_party/pyqtlet2/leaflet/layer/featuregroup.py +34 -0
- beratools/third_party/pyqtlet2/leaflet/layer/icon/__init__.py +1 -0
- beratools/third_party/pyqtlet2/leaflet/layer/icon/icon.py +30 -0
- beratools/third_party/pyqtlet2/leaflet/layer/imageoverlay.py +18 -0
- beratools/third_party/pyqtlet2/leaflet/layer/layer.py +105 -0
- beratools/third_party/pyqtlet2/leaflet/layer/layergroup.py +45 -0
- beratools/third_party/pyqtlet2/leaflet/layer/marker/__init__.py +1 -0
- beratools/third_party/pyqtlet2/leaflet/layer/marker/marker.py +91 -0
- beratools/third_party/pyqtlet2/leaflet/layer/tile/__init__.py +2 -0
- beratools/third_party/pyqtlet2/leaflet/layer/tile/gridlayer.py +4 -0
- beratools/third_party/pyqtlet2/leaflet/layer/tile/tilelayer.py +16 -0
- beratools/third_party/pyqtlet2/leaflet/layer/vector/__init__.py +5 -0
- beratools/third_party/pyqtlet2/leaflet/layer/vector/circle.py +15 -0
- beratools/third_party/pyqtlet2/leaflet/layer/vector/circlemarker.py +18 -0
- beratools/third_party/pyqtlet2/leaflet/layer/vector/path.py +5 -0
- beratools/third_party/pyqtlet2/leaflet/layer/vector/polygon.py +14 -0
- beratools/third_party/pyqtlet2/leaflet/layer/vector/polyline.py +18 -0
- beratools/third_party/pyqtlet2/leaflet/layer/vector/rectangle.py +14 -0
- beratools/third_party/pyqtlet2/leaflet/map/__init__.py +1 -0
- beratools/third_party/pyqtlet2/leaflet/map/map.py +220 -0
- beratools/third_party/pyqtlet2/mapwidget.py +45 -0
- beratools/third_party/pyqtlet2/web/custom.js +43 -0
- beratools/third_party/pyqtlet2/web/map.html +23 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-shadow.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.css +656 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.js +6 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.codeclimate.yml +14 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.editorconfig +4 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.gitattributes +22 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.travis.yml +43 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/LICENSE +20 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-shadow.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.svg +156 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.css +10 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.js +10 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/LICENSE +22 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/leaflet.rotatedMarker.js +57 -0
- beratools/tools/Beratools_r_script.r +1120 -0
- beratools/tools/Ht_metrics.py +116 -0
- beratools/tools/__init__.py +7 -0
- beratools/tools/batch_processing.py +132 -0
- beratools/tools/canopy_threshold_relative.py +670 -0
- beratools/tools/canopycostraster.py +222 -0
- beratools/tools/centerline.py +176 -0
- beratools/tools/common.py +885 -0
- beratools/tools/fl_regen_csf.py +428 -0
- beratools/tools/forest_line_attributes.py +408 -0
- beratools/tools/forest_line_ecosite.py +216 -0
- beratools/tools/lapis_all.py +103 -0
- beratools/tools/least_cost_path_from_chm.py +152 -0
- beratools/tools/line_footprint_absolute.py +363 -0
- beratools/tools/line_footprint_fixed.py +282 -0
- beratools/tools/line_footprint_functions.py +720 -0
- beratools/tools/line_footprint_relative.py +64 -0
- beratools/tools/ln_relative_metrics.py +615 -0
- beratools/tools/r_cal_lpi_elai.r +25 -0
- beratools/tools/r_generate_pd_focalraster.r +101 -0
- beratools/tools/r_interface.py +80 -0
- beratools/tools/r_point_density.r +9 -0
- beratools/tools/rpy_chm2trees.py +86 -0
- beratools/tools/rpy_dsm_chm_by.py +81 -0
- beratools/tools/rpy_dtm_by.py +63 -0
- beratools/tools/rpy_find_cellsize.py +43 -0
- beratools/tools/rpy_gnd_csf.py +74 -0
- beratools/tools/rpy_hummock_hollow.py +85 -0
- beratools/tools/rpy_hummock_hollow_raster.py +71 -0
- beratools/tools/rpy_las_info.py +51 -0
- beratools/tools/rpy_laz2las.py +40 -0
- beratools/tools/rpy_lpi_elai_lascat.py +466 -0
- beratools/tools/rpy_normalized_lidar_by.py +56 -0
- beratools/tools/rpy_percent_above_dbh.py +80 -0
- beratools/tools/rpy_points2trees.py +88 -0
- beratools/tools/rpy_vegcoverage.py +94 -0
- beratools/tools/tiler.py +206 -0
- beratools/tools/tool_template.py +54 -0
- beratools/tools/vertex_optimization.py +620 -0
- beratools/tools/zonal_threshold.py +144 -0
- beratools-0.2.0.dist-info/METADATA +63 -0
- beratools-0.2.0.dist-info/RECORD +142 -0
- beratools-0.2.0.dist-info/WHEEL +4 -0
- beratools-0.2.0.dist-info/entry_points.txt +2 -0
- beratools-0.2.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import numpy as np
|
|
5
|
+
from numpy.lib.stride_tricks import as_strided
|
|
6
|
+
|
|
7
|
+
import rasterio
|
|
8
|
+
import xarray as xr
|
|
9
|
+
from xrspatial import convolution, focal
|
|
10
|
+
from scipy import ndimage
|
|
11
|
+
|
|
12
|
+
from common import *
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# TODO: Rolling Statistics for grid data... an alternative
|
|
16
|
+
# by Dan Patterson
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _check(a, r_c, subok=False):
|
|
20
|
+
"""Performs the array checks necessary for stride and block.
|
|
21
|
+
: in_array - Array or list.
|
|
22
|
+
: r_c - tuple/list/array of rows x cols.
|
|
23
|
+
: subok - from numpy 1.12 added, keep for now
|
|
24
|
+
:Returns:
|
|
25
|
+
:------
|
|
26
|
+
:Attempts will be made to ...
|
|
27
|
+
: produce a shape at least (1*c). For a scalar, the
|
|
28
|
+
: minimum shape will be (1*r) for 1D array or (1*c) for 2D
|
|
29
|
+
: array if r<c. Be aware
|
|
30
|
+
"""
|
|
31
|
+
if isinstance(r_c, (int, float)):
|
|
32
|
+
r_c = (1, int(r_c))
|
|
33
|
+
|
|
34
|
+
r, c = r_c
|
|
35
|
+
if a.ndim == 1:
|
|
36
|
+
a = np.atleast_2d(a)
|
|
37
|
+
|
|
38
|
+
r, c = r_c = (min(r, a.shape[0]), min(c, a.shape[1]))
|
|
39
|
+
a = np.array(a, copy=False, subok=subok)
|
|
40
|
+
return a, r, c, tuple(r_c)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _pad(in_array, kernel):
|
|
44
|
+
"""Pad a sliding array to allow for stats"""
|
|
45
|
+
pad_x = int(kernel.shape[0] / 2)
|
|
46
|
+
pad_y = int(kernel.shape[0] / 2)
|
|
47
|
+
result = np.pad(in_array, pad_width=(pad_x, pad_y), mode="constant", constant_values=(np.NaN, np.NaN))
|
|
48
|
+
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def stride(a, r_c):
|
|
53
|
+
"""Provide a 2D sliding/moving view of an array.
|
|
54
|
+
: There is no edge correction for outputs.
|
|
55
|
+
:
|
|
56
|
+
:Requires:
|
|
57
|
+
:--------
|
|
58
|
+
: _check(a, r_c) ... Runs the checks on the inputs.
|
|
59
|
+
: a - array or list, usually a 2D array. Assumes rows is >=1,
|
|
60
|
+
: it is corrected as is the number of columns.
|
|
61
|
+
: r_c - tuple/list/array of rows x cols. Attempts to
|
|
62
|
+
: produce a shape at least (1*c). For a scalar, the
|
|
63
|
+
: minimum shape will be (1*r) for 1D array or 2D
|
|
64
|
+
: array if r<c. Be aware
|
|
65
|
+
"""
|
|
66
|
+
a, r, c, r_c = _check(a, r_c)
|
|
67
|
+
shape = (a.shape[0] - r + 1, a.shape[1] - c + 1) + r_c
|
|
68
|
+
strides = a.strides * 2
|
|
69
|
+
a_s = (as_strided(a, shape=shape, strides=strides)).squeeze()
|
|
70
|
+
return a_s
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def normalize_chm(raster):
|
|
74
|
+
n_raster = np.where(raster >= 0, raster, 0)
|
|
75
|
+
return n_raster
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def np_cc_map(out_canopy_r, chm, in_array, min_ht):
|
|
79
|
+
print('Generating Canopy Closure Raster ...')
|
|
80
|
+
|
|
81
|
+
# canopy_ndarray = np.where(in_array >= min_ht, 1., 0.).astype(float)
|
|
82
|
+
canopy_ndarray = np.ma.where(in_array > min_ht, 1., 0.).astype(float)
|
|
83
|
+
canopy_ndarray = np.ma.filled(canopy_ndarray, chm.nodata)
|
|
84
|
+
try:
|
|
85
|
+
write_canopy = rasterio.open(out_canopy_r, 'w', **chm.profile)
|
|
86
|
+
write_canopy.write(canopy_ndarray, 1)
|
|
87
|
+
write_canopy.close()
|
|
88
|
+
print('Generating Canopy Closure (CC) Raster ... Done')
|
|
89
|
+
except Exception as e:
|
|
90
|
+
print(sys.exc_info())
|
|
91
|
+
del in_array
|
|
92
|
+
return canopy_ndarray
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def fs_raster(in_ndarray, kernel):
|
|
96
|
+
print('Generating Canopy Closure Focal Statistic ...')
|
|
97
|
+
padded_array = _pad(in_ndarray, kernel)
|
|
98
|
+
a_s = stride(padded_array, kernel.shape)
|
|
99
|
+
|
|
100
|
+
# TODO: np.where on large ndarray fail (allocate memory error)
|
|
101
|
+
a_s_masked = np.where(kernel == 1, a_s, np.NaN)
|
|
102
|
+
print("Calculating Canopy Closure's Focal Statistic-Mean ...")
|
|
103
|
+
mean_result = np.nanmean(a_s_masked, axis=(2, 3))
|
|
104
|
+
print("Calculating Canopy Closure's Focal Statistic-Stand Deviation Raster ...")
|
|
105
|
+
stdev_result = np.nanstd(a_s_masked, axis=(2, 3))
|
|
106
|
+
del a_s
|
|
107
|
+
return mean_result, stdev_result
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def fs_raster_stdmean(in_ndarray, kernel, nodata):
|
|
111
|
+
# This function uses xrspatial whcih can handle large data but slow
|
|
112
|
+
in_ndarray[in_ndarray == nodata] = np.nan
|
|
113
|
+
result_ndarray = focal.focal_stats(xr.DataArray(in_ndarray), kernel, stats_funcs=['std', 'mean'])
|
|
114
|
+
|
|
115
|
+
# Flattening the array
|
|
116
|
+
flatten_std_result_ndarray = result_ndarray[0].data.reshape(-1)
|
|
117
|
+
flatten_mean_result_ndarray = result_ndarray[1].data.reshape(-1)
|
|
118
|
+
|
|
119
|
+
# Re-shaping the array
|
|
120
|
+
reshape_std_ndarray = flatten_std_result_ndarray.reshape(in_ndarray.shape[0], in_ndarray.shape[1])
|
|
121
|
+
reshape_mean_ndarray = flatten_mean_result_ndarray.reshape(in_ndarray.shape[0], in_ndarray.shape[1])
|
|
122
|
+
return reshape_std_ndarray, reshape_mean_ndarray
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def smooth_cost(in_raster, search_dist, sampling):
|
|
126
|
+
print('Generating Cost Raster ...')
|
|
127
|
+
from tempfile import mkdtemp
|
|
128
|
+
import os.path as path
|
|
129
|
+
import shutil
|
|
130
|
+
|
|
131
|
+
euc_dist_array = None
|
|
132
|
+
row, col = in_raster.shape
|
|
133
|
+
if row * col >= 30000 * 30000:
|
|
134
|
+
filename = path.join(mkdtemp(), 'tempmmemap.dat')
|
|
135
|
+
a_in_mat = np.memmap(filename, in_raster.dtype, 'w+', shape=in_raster.shape)
|
|
136
|
+
a_in_mat[:] = in_raster[:]
|
|
137
|
+
a_in_mat.flush()
|
|
138
|
+
euc_dist_array = ndimage.distance_transform_edt(np.logical_not(a_in_mat), sampling=sampling)
|
|
139
|
+
del a_in_mat, in_raster
|
|
140
|
+
shutil.rmtree(path.dirname(filename))
|
|
141
|
+
else:
|
|
142
|
+
euc_dist_array = ndimage.distance_transform_edt(np.logical_not(in_raster), sampling=sampling)
|
|
143
|
+
|
|
144
|
+
smooth1 = float(search_dist) - euc_dist_array
|
|
145
|
+
smooth1[smooth1 <= 0.0] = 0.0
|
|
146
|
+
smooth_cost_array = smooth1 / float(search_dist)
|
|
147
|
+
|
|
148
|
+
return smooth_cost_array
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def np_cost_raster(canopy_ndarray, cc_mean, cc_std, cc_smooth, chm, avoidance, cost_raster_exponent, out_cost_r):
|
|
152
|
+
print('Generating Smoothed Cost Raster ...')
|
|
153
|
+
aM1a = (cc_mean - cc_std)
|
|
154
|
+
aM1b = (cc_mean + cc_std)
|
|
155
|
+
aM1 = np.divide(aM1a, aM1b, where=aM1b != 0, out=np.zeros(aM1a.shape, dtype=float))
|
|
156
|
+
aM = (1 + aM1) / 2
|
|
157
|
+
aaM = (cc_mean + cc_std)
|
|
158
|
+
bM = np.where(aaM <= 0, 0, aM)
|
|
159
|
+
cM = bM * (1 - avoidance) + (cc_smooth * avoidance)
|
|
160
|
+
dM = np.where(canopy_ndarray == 1, 1, cM)
|
|
161
|
+
eM = np.exp(dM)
|
|
162
|
+
result = np.power(eM, float(cost_raster_exponent))
|
|
163
|
+
write_cost = rasterio.open(out_cost_r, 'w+', driver='GTiff', height=chm.shape[0], width=chm.shape[1],
|
|
164
|
+
count=1, dtype=chm.read(1).dtype, crs=chm.crs, transform=chm.transform)
|
|
165
|
+
write_cost.write(result, 1)
|
|
166
|
+
write_cost.close()
|
|
167
|
+
print('Generating Smoothed Cost Raster ... Done')
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# TODO: deal with NODATA
|
|
172
|
+
def canopy_cost_raster(callback, in_chm, canopy_ht_threshold, tree_radius, max_line_dist,
|
|
173
|
+
canopy_avoidance, exponent, out_canopy, out_cost, processes, verbose):
|
|
174
|
+
canopy_ht_threshold = float(canopy_ht_threshold)
|
|
175
|
+
tree_radius = float(tree_radius)
|
|
176
|
+
max_line_dist = float(max_line_dist)
|
|
177
|
+
canopy_avoidance = float(canopy_avoidance)
|
|
178
|
+
cost_raster_exponent = float(exponent)
|
|
179
|
+
|
|
180
|
+
print('In CHM: ' + in_chm)
|
|
181
|
+
chm = rasterio.open(in_chm)
|
|
182
|
+
(cell_x, cell_y) = chm.res
|
|
183
|
+
|
|
184
|
+
print('Loading CHM ...')
|
|
185
|
+
band1_ndarray = chm.read(1, masked=True)
|
|
186
|
+
print('%{}'.format(10))
|
|
187
|
+
|
|
188
|
+
print('Preparing Kernel window ...')
|
|
189
|
+
kernel = convolution.circle_kernel(cell_x, cell_y, tree_radius)
|
|
190
|
+
print('%{}'.format(20))
|
|
191
|
+
|
|
192
|
+
# Generate Canopy Raster and return the Canopy array
|
|
193
|
+
canopy_ndarray = np_cc_map(out_canopy, chm, band1_ndarray, canopy_ht_threshold)
|
|
194
|
+
print('%{}'.format(40))
|
|
195
|
+
print('Apply focal statistic on raster ...')
|
|
196
|
+
|
|
197
|
+
# Calculating focal statistic from canopy raster
|
|
198
|
+
# Alternative: (only work on large cell size
|
|
199
|
+
if cell_y > 1 and cell_x > 1:
|
|
200
|
+
cc_mean, cc_std = fs_raster(canopy_ndarray, kernel)
|
|
201
|
+
else:
|
|
202
|
+
cc_std, cc_mean = fs_raster_stdmean(canopy_ndarray, kernel, chm.nodata)
|
|
203
|
+
print('%{}'.format(60))
|
|
204
|
+
print('Apply focal statistic on raster ... Done')
|
|
205
|
+
|
|
206
|
+
# Smoothing raster
|
|
207
|
+
cc_smooth = smooth_cost(canopy_ndarray, max_line_dist, [cell_x, cell_y])
|
|
208
|
+
avoidance = max(min(float(canopy_avoidance), 1), 0)
|
|
209
|
+
np_cost_raster(canopy_ndarray, cc_mean, cc_std, cc_smooth, chm, avoidance, cost_raster_exponent, out_cost)
|
|
210
|
+
print('%{}'.format(100))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == '__main__':
|
|
214
|
+
in_args, in_verbose = check_arguments()
|
|
215
|
+
|
|
216
|
+
start_time = time.time()
|
|
217
|
+
print('Starting Canopy and Cost Raster processing\n @ {}'
|
|
218
|
+
.format(time.strftime("%d %b %Y %H:%M:%S", time.localtime())))
|
|
219
|
+
|
|
220
|
+
canopy_cost_raster(print, **in_args.input, processes=int(in_args.processes), verbose=in_verbose)
|
|
221
|
+
print('Canopy and Cost Raster processing is done in {} seconds)'
|
|
222
|
+
.format(round(time.time() - start_time, 5)))
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from inspect import getsourcefile
|
|
7
|
+
|
|
8
|
+
if __name__ == "__main__":
|
|
9
|
+
current_file = Path(getsourcefile(lambda: 0)).resolve()
|
|
10
|
+
btool_dir = current_file.parents[2]
|
|
11
|
+
sys.path.insert(0, btool_dir.as_posix())
|
|
12
|
+
|
|
13
|
+
from beratools.core.logger import Logger
|
|
14
|
+
from beratools.core.algo_centerline import *
|
|
15
|
+
from beratools.core.dijkstra_algorithm import *
|
|
16
|
+
from beratools.core.constants import *
|
|
17
|
+
from common import *
|
|
18
|
+
|
|
19
|
+
log = Logger('centerline', file_level=logging.INFO)
|
|
20
|
+
logger = log.get_logger()
|
|
21
|
+
print = log.print
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def process_single_line(line_args):
|
|
25
|
+
line = line_args[0][0]
|
|
26
|
+
prop = line_args[0][1]
|
|
27
|
+
line_radius = line_args[1]
|
|
28
|
+
in_raster = line_args[2]
|
|
29
|
+
line_id = line_args[3]
|
|
30
|
+
seed_line = shape(line) # LineString
|
|
31
|
+
line_radius = float(line_radius)
|
|
32
|
+
|
|
33
|
+
default_return = (seed_line, seed_line, prop, None)
|
|
34
|
+
|
|
35
|
+
cost_clip, out_meta = clip_raster(in_raster, seed_line, line_radius)
|
|
36
|
+
|
|
37
|
+
if not HAS_COST_RASTER:
|
|
38
|
+
cost_clip, _ = cost_raster(cost_clip, out_meta)
|
|
39
|
+
|
|
40
|
+
lc_path = line
|
|
41
|
+
try:
|
|
42
|
+
if CL_USE_SKIMAGE_GRAPH:
|
|
43
|
+
# skimage shortest path
|
|
44
|
+
lc_path = find_least_cost_path_skimage(cost_clip, out_meta, seed_line)
|
|
45
|
+
else:
|
|
46
|
+
lc_path = find_least_cost_path(cost_clip, out_meta, seed_line)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(e)
|
|
49
|
+
return default_return
|
|
50
|
+
|
|
51
|
+
if lc_path:
|
|
52
|
+
lc_path_coords = lc_path.coords
|
|
53
|
+
else:
|
|
54
|
+
lc_path_coords = []
|
|
55
|
+
|
|
56
|
+
# search for centerline
|
|
57
|
+
if len(lc_path_coords) < 2:
|
|
58
|
+
print('No least cost path detected, use input line.')
|
|
59
|
+
prop['status'] = CenterlineStatus.FAILED.value
|
|
60
|
+
return default_return
|
|
61
|
+
|
|
62
|
+
# get corridor raster
|
|
63
|
+
lc_path = LineString(lc_path_coords)
|
|
64
|
+
cost_clip, out_meta = clip_raster(in_raster, lc_path, line_radius * 0.9)
|
|
65
|
+
if not HAS_COST_RASTER:
|
|
66
|
+
cost_clip, _ = cost_raster(cost_clip, out_meta)
|
|
67
|
+
|
|
68
|
+
out_transform = out_meta['transform']
|
|
69
|
+
transformer = rasterio.transform.AffineTransformer(out_transform)
|
|
70
|
+
cell_size = (out_transform[0], -out_transform[4])
|
|
71
|
+
|
|
72
|
+
x1, y1 = lc_path_coords[0]
|
|
73
|
+
x2, y2 = lc_path_coords[-1]
|
|
74
|
+
source = [transformer.rowcol(x1, y1)]
|
|
75
|
+
destination = [transformer.rowcol(x2, y2)]
|
|
76
|
+
corridor_thresh_cl = corridor_raster(cost_clip, out_meta, source, destination,
|
|
77
|
+
cell_size, FP_CORRIDOR_THRESHOLD)
|
|
78
|
+
|
|
79
|
+
# find contiguous corridor polygon and extract centerline
|
|
80
|
+
df = gpd.GeoDataFrame(geometry=[seed_line], crs=out_meta['crs'])
|
|
81
|
+
corridor_poly_gpd = find_corridor_polygon(corridor_thresh_cl, out_transform, df)
|
|
82
|
+
center_line, status = find_centerline(corridor_poly_gpd.geometry.iloc[0], lc_path)
|
|
83
|
+
prop['status'] = status.value
|
|
84
|
+
|
|
85
|
+
print(" Searching centerline: line {} ".format(line_id), flush=True)
|
|
86
|
+
return center_line, lc_path, prop, corridor_poly_gpd
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def centerline(callback, in_line, in_raster, line_radius,
|
|
90
|
+
proc_segments, out_line, processes, verbose):
|
|
91
|
+
if not compare_crs(vector_crs(in_line), raster_crs(in_raster)):
|
|
92
|
+
print("Line and CHM have different spatial references, please check.")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# Read input line features
|
|
96
|
+
layer_crs = None
|
|
97
|
+
schema = None
|
|
98
|
+
input_lines = []
|
|
99
|
+
|
|
100
|
+
with fiona.open(in_line) as open_line_file:
|
|
101
|
+
layer_crs = open_line_file.crs
|
|
102
|
+
schema = open_line_file.meta['schema']
|
|
103
|
+
for line in open_line_file:
|
|
104
|
+
if line.geometry:
|
|
105
|
+
if line.geometry['type'] != 'MultiLineString':
|
|
106
|
+
input_lines.append([line.geometry, line.properties])
|
|
107
|
+
else:
|
|
108
|
+
print('MultiLineString found.')
|
|
109
|
+
geoms = shape(line['geometry']).geoms
|
|
110
|
+
for item in geoms:
|
|
111
|
+
line_part = fiona.Geometry.from_dict(item)
|
|
112
|
+
if line_part:
|
|
113
|
+
input_lines.append([line_part, line.properties])
|
|
114
|
+
else:
|
|
115
|
+
print(f'Line {line.id} has empty geometry.')
|
|
116
|
+
|
|
117
|
+
if proc_segments:
|
|
118
|
+
# split line segments at vertices
|
|
119
|
+
input_lines_temp = []
|
|
120
|
+
for line in input_lines:
|
|
121
|
+
line_seg = line[0]
|
|
122
|
+
line_prop = line[1]
|
|
123
|
+
line_segs = segments(line_seg.coordinates)
|
|
124
|
+
line_feats = [(line, line_prop) for line in line_segs]
|
|
125
|
+
if line_segs:
|
|
126
|
+
input_lines_temp.extend(line_feats)
|
|
127
|
+
|
|
128
|
+
input_lines = input_lines_temp
|
|
129
|
+
|
|
130
|
+
# Process lines
|
|
131
|
+
all_lines = []
|
|
132
|
+
i = 0
|
|
133
|
+
for line in input_lines:
|
|
134
|
+
all_lines.append((line, line_radius, in_raster, i))
|
|
135
|
+
i += 1
|
|
136
|
+
|
|
137
|
+
print('{} lines to be processed.'.format(len(all_lines)))
|
|
138
|
+
|
|
139
|
+
lc_path_geoms = []
|
|
140
|
+
feat_props = []
|
|
141
|
+
center_line_geoms = []
|
|
142
|
+
corridor_poly_list = []
|
|
143
|
+
result = execute_multiprocessing(process_single_line, all_lines, 'Centerline',
|
|
144
|
+
processes, 1, verbose=verbose)
|
|
145
|
+
|
|
146
|
+
for item in result:
|
|
147
|
+
center_line = item[0]
|
|
148
|
+
lc_path = item[1]
|
|
149
|
+
prop = item[2]
|
|
150
|
+
corridor_poly = item[3]
|
|
151
|
+
|
|
152
|
+
if lc_path and prop:
|
|
153
|
+
lc_path_geoms.append(lc_path)
|
|
154
|
+
feat_props.append(prop)
|
|
155
|
+
center_line_geoms.append(center_line)
|
|
156
|
+
corridor_poly_list.append(corridor_poly)
|
|
157
|
+
|
|
158
|
+
out_centerline_path = Path(out_line)
|
|
159
|
+
schema['properties']['status'] = 'int'
|
|
160
|
+
save_features_to_shapefile(out_centerline_path.as_posix(), layer_crs, center_line_geoms, feat_props, schema)
|
|
161
|
+
|
|
162
|
+
out_least_cost_path = out_centerline_path.with_stem(out_centerline_path.stem + '_least_cost_path')
|
|
163
|
+
save_features_to_shapefile(out_least_cost_path.as_posix(), layer_crs, lc_path_geoms, feat_props, schema)
|
|
164
|
+
|
|
165
|
+
# save corridor polygons
|
|
166
|
+
corridor_polys = pd.concat(corridor_poly_list)
|
|
167
|
+
out_corridor_poly_path = Path(out_line)
|
|
168
|
+
out_corridor_poly_path = out_corridor_poly_path.with_stem(out_corridor_poly_path.stem + '_corridor_poly')
|
|
169
|
+
corridor_polys.to_file(out_corridor_poly_path.as_posix())
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == '__main__':
|
|
173
|
+
in_args, in_verbose = check_arguments()
|
|
174
|
+
start_time = time.time()
|
|
175
|
+
centerline(print, **in_args.input, processes=int(in_args.processes), verbose=in_verbose)
|
|
176
|
+
print('Elapsed time: {}'.format(time.time() - start_time))
|