pyopenrivercam 0.8.10__py3-none-any.whl → 0.8.12__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.
- {pyopenrivercam-0.8.10.dist-info → pyopenrivercam-0.8.12.dist-info}/METADATA +1 -1
- {pyopenrivercam-0.8.10.dist-info → pyopenrivercam-0.8.12.dist-info}/RECORD +12 -12
- pyorc/__init__.py +1 -1
- pyorc/api/cross_section.py +307 -23
- pyorc/api/mask.py +29 -28
- pyorc/api/plot.py +14 -8
- pyorc/api/transect.py +65 -73
- pyorc/cv.py +38 -5
- pyorc/service/velocimetry.py +37 -19
- {pyopenrivercam-0.8.10.dist-info → pyopenrivercam-0.8.12.dist-info}/WHEEL +0 -0
- {pyopenrivercam-0.8.10.dist-info → pyopenrivercam-0.8.12.dist-info}/entry_points.txt +0 -0
- {pyopenrivercam-0.8.10.dist-info → pyopenrivercam-0.8.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
pyorc/__init__.py,sha256=
|
|
1
|
+
pyorc/__init__.py,sha256=osrK3G2RA5IsF1ka-QHSPa7gXf7JCXQuUWK8RnR1I1A,524
|
|
2
2
|
pyorc/const.py,sha256=Ia0KRkm-E1lJk4NxQVPDIfN38EBB7BKvxmwIHJrGPUY,2597
|
|
3
|
-
pyorc/cv.py,sha256=
|
|
3
|
+
pyorc/cv.py,sha256=fXGqT8vBn9-z6UxS5ho7thP9VQll9RrYHJW5KnUJQjo,50250
|
|
4
4
|
pyorc/helpers.py,sha256=90TDtka0ydAydv3g5Dfc8MgtuSt0_9D9-HOtffpcBds,30636
|
|
5
5
|
pyorc/plot_helpers.py,sha256=gLKslspsF_Z4jib5jkBv2wRjKnHTbuRFgkp_PCmv-uU,1803
|
|
6
6
|
pyorc/project.py,sha256=CGKfICkQEpFRmh_ZeDEfbQ-wefJt7teWJd6B5IPF038,7747
|
|
@@ -8,12 +8,12 @@ pyorc/pyorc.sh,sha256=-xOSUNnMAwVbdNkjKNKMZMaBljWsGLhadG-j0DNlJP4,5
|
|
|
8
8
|
pyorc/sample_data.py,sha256=53NVnVmEksDw8ilbfhFFCiFJiGAIpxdgREbA_xt8P3o,2508
|
|
9
9
|
pyorc/api/__init__.py,sha256=k2OQQH4NrtXTuVm23d0g_SX6H5DhnKC9_kDyzJ4dWdk,428
|
|
10
10
|
pyorc/api/cameraconfig.py,sha256=NP9F7LhPO3aO6FRWkrGl6XpX8O3K59zfTtaYR3Kujqw,65419
|
|
11
|
-
pyorc/api/cross_section.py,sha256=
|
|
11
|
+
pyorc/api/cross_section.py,sha256=MH0AEw5K1Kc1ClZeQRBUYkShZYVk41fshLn6GzCZAas,65212
|
|
12
12
|
pyorc/api/frames.py,sha256=Kls4mpU_4hoUaXs9DJf2S6RHyp2D5emXJkAQWvvT39U,24300
|
|
13
|
-
pyorc/api/mask.py,sha256
|
|
13
|
+
pyorc/api/mask.py,sha256=-owe66kte2ob3_Zndf21JR-LyEX_1mECbHOuqNfzuMc,16507
|
|
14
14
|
pyorc/api/orcbase.py,sha256=C23QTKOyxHUafyJsq_t7xn_BzAEvf4DDfzlYAopons8,4189
|
|
15
|
-
pyorc/api/plot.py,sha256
|
|
16
|
-
pyorc/api/transect.py,sha256=
|
|
15
|
+
pyorc/api/plot.py,sha256=MxIEIS8l46bUaca0GtMazx8-k2_TbfQLrPCPAjuWos8,31082
|
|
16
|
+
pyorc/api/transect.py,sha256=wENKWt0u0lHtT0lPYv47faHf_iAN9Mmeev-vwWjnz6E,13382
|
|
17
17
|
pyorc/api/velocimetry.py,sha256=bfU_XPbUbrdBI2XGprzh_3YADbGHfy4OuS1oBlbLEEI,12047
|
|
18
18
|
pyorc/api/video.py,sha256=lGD6bcV6Uu2u3zuGF_m3KxX2Cyp9k-YHUiXA42TOE3E,22458
|
|
19
19
|
pyorc/cli/__init__.py,sha256=A7hOQV26vIccPnDc8L2KqoJOSpMpf2PiMOXS18pAsWg,32
|
|
@@ -23,12 +23,12 @@ pyorc/cli/log.py,sha256=Vg8GznmrEPqijfW6wv4OCl8R00Ld_fVt-ULTitaDijY,2824
|
|
|
23
23
|
pyorc/cli/main.py,sha256=qhAZkUuAViCpHh9c19tpcpbs_xoZJkYHhOsEXJBFXfM,12742
|
|
24
24
|
pyorc/service/__init__.py,sha256=vPrzFlZ4e_GjnibwW6-k8KDz3b7WpgmGcwSDk0mr13Y,55
|
|
25
25
|
pyorc/service/camera_config.py,sha256=OsRLpe5jd-lu6HT4Vx5wEg554CMS-IKz-q62ir4VbPo,2375
|
|
26
|
-
pyorc/service/velocimetry.py,sha256=
|
|
26
|
+
pyorc/service/velocimetry.py,sha256=bPI1OdN_fi0gZES08mb7yqCS_4I-lKSZ2JvWSGTRD1E,34434
|
|
27
27
|
pyorc/velocimetry/__init__.py,sha256=lYM7oJZWxgJ2SpE22xhy7pBYcgkKFHMBHYmDvvMbtZk,148
|
|
28
28
|
pyorc/velocimetry/ffpiv.py,sha256=CYUjgwnB5osQmJ83j3E00B9P0_hS-rFuhyvufxKXtag,17487
|
|
29
29
|
pyorc/velocimetry/openpiv.py,sha256=6BxsbXLzT4iEq7v08G4sOhVlYFodUpY6sIm3jdCxNMs,13149
|
|
30
|
-
pyopenrivercam-0.8.
|
|
31
|
-
pyopenrivercam-0.8.
|
|
32
|
-
pyopenrivercam-0.8.
|
|
33
|
-
pyopenrivercam-0.8.
|
|
34
|
-
pyopenrivercam-0.8.
|
|
30
|
+
pyopenrivercam-0.8.12.dist-info/entry_points.txt,sha256=Cv_WI2Y6QLnPiNCXGli0gS4WAOAeMoprha1rAR3vdRE,44
|
|
31
|
+
pyopenrivercam-0.8.12.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
|
32
|
+
pyopenrivercam-0.8.12.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
33
|
+
pyopenrivercam-0.8.12.dist-info/METADATA,sha256=hU0j9dsG6ksjRTM6UdMLVJ15TGzGQ9lW1mHhxDKxJy0,11641
|
|
34
|
+
pyopenrivercam-0.8.12.dist-info/RECORD,,
|
pyorc/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""pyorc: free and open-source image-based surface velocity and discharge."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.8.
|
|
3
|
+
__version__ = "0.8.12"
|
|
4
4
|
|
|
5
5
|
from .api import CameraConfig, CrossSection, Frames, Transect, Velocimetry, Video, get_camera_config, load_camera_config # noqa
|
|
6
6
|
from .project import * # noqa
|
pyorc/api/cross_section.py
CHANGED
|
@@ -13,7 +13,7 @@ from matplotlib import patheffects
|
|
|
13
13
|
from scipy.interpolate import interp1d
|
|
14
14
|
from scipy.optimize import differential_evolution
|
|
15
15
|
from shapely import affinity, geometry
|
|
16
|
-
from shapely.ops import polygonize
|
|
16
|
+
from shapely.ops import polygonize, split
|
|
17
17
|
|
|
18
18
|
from pyorc import cv, plot_helpers
|
|
19
19
|
|
|
@@ -278,7 +278,7 @@ class CrossSection:
|
|
|
278
278
|
Parameters
|
|
279
279
|
----------
|
|
280
280
|
h : float
|
|
281
|
-
water level [m]
|
|
281
|
+
water level [m].
|
|
282
282
|
sz : bool, optional
|
|
283
283
|
If set, return water level line in y-z projection, by default False.
|
|
284
284
|
extend_by : float, optional
|
|
@@ -613,16 +613,23 @@ class CrossSection:
|
|
|
613
613
|
raise ValueError("Amount of water line crossings must be 2 for a planar surface estimate.")
|
|
614
614
|
return geometry.Polygon(list(wls[0].coords) + list(wls[1].coords[::-1]))
|
|
615
615
|
|
|
616
|
-
def get_wetted_surface_sz(
|
|
617
|
-
|
|
616
|
+
def get_wetted_surface_sz(
|
|
617
|
+
self, h: float, perimeter: bool = False
|
|
618
|
+
) -> Union[geometry.MultiPolygon, geometry.MultiLineString]:
|
|
619
|
+
"""Retrieve a wetted surface or perimeter perpendicular to flow direction (SZ) for a water level.
|
|
620
|
+
|
|
621
|
+
This returns a `geometry.MultiPolygon` when a surface is requested (`perimeter=False`), and
|
|
622
|
+
`geometry.MultiLineString` when a perimeter is requested (`perimeter=True`).
|
|
618
623
|
|
|
619
|
-
This is a useful method for instance to estimate m2 wetted surface
|
|
620
|
-
section.
|
|
624
|
+
This is a useful method for instance to estimate m2 wetted surface or m wetted perimeter length for a given
|
|
625
|
+
water level in the cross section.
|
|
621
626
|
|
|
622
627
|
Parameters
|
|
623
628
|
----------
|
|
624
629
|
h : float
|
|
625
630
|
water level [m]
|
|
631
|
+
perimeter : bool, optional
|
|
632
|
+
If set to True, return a linestring with the wetted perimeter instead.
|
|
626
633
|
|
|
627
634
|
Returns
|
|
628
635
|
-------
|
|
@@ -630,6 +637,12 @@ class CrossSection:
|
|
|
630
637
|
Wetted surface as a polygon, in Y-Z projection.
|
|
631
638
|
|
|
632
639
|
"""
|
|
640
|
+
|
|
641
|
+
def avg_y(line):
|
|
642
|
+
"""Compute average y-coordinate of a line."""
|
|
643
|
+
ys = [p[1] for p in line.coords]
|
|
644
|
+
return sum(ys) / len(ys)
|
|
645
|
+
|
|
633
646
|
wl = self.get_cs_waterlevel(
|
|
634
647
|
h=h, sz=True, extend_by=0.1
|
|
635
648
|
) # extend a small bit to guarantee crossing with the bottom coordinates
|
|
@@ -642,6 +655,18 @@ class CrossSection:
|
|
|
642
655
|
if bottom_points[-1].y < zl:
|
|
643
656
|
bottom_points.append(geometry.Point(bottom_points[-1].x, zl + 0.1))
|
|
644
657
|
bottom_line = geometry.LineString(bottom_points)
|
|
658
|
+
if perimeter:
|
|
659
|
+
wl_z = wl.coords[0][-1]
|
|
660
|
+
split_segments = split(bottom_line, wl)
|
|
661
|
+
filtered = []
|
|
662
|
+
for seg in split_segments.geoms:
|
|
663
|
+
seg_z = avg_y(seg)
|
|
664
|
+
if seg_z < wl_z:
|
|
665
|
+
# segment is below water level, add to perimeter
|
|
666
|
+
filtered.append(seg)
|
|
667
|
+
|
|
668
|
+
return geometry.MultiLineString(filtered)
|
|
669
|
+
# return wetted_perim
|
|
645
670
|
pol = list(polygonize(wl.union(bottom_line)))
|
|
646
671
|
if len(pol) == 0:
|
|
647
672
|
# create infinitely small polygon at lowest z coordinate
|
|
@@ -1074,6 +1099,182 @@ class CrossSection:
|
|
|
1074
1099
|
)
|
|
1075
1100
|
return ax
|
|
1076
1101
|
|
|
1102
|
+
def _preprocess_level_range(
|
|
1103
|
+
self,
|
|
1104
|
+
min_h: Optional[float] = None,
|
|
1105
|
+
max_h: Optional[float] = None,
|
|
1106
|
+
min_z: Optional[float] = None,
|
|
1107
|
+
max_z: Optional[float] = None,
|
|
1108
|
+
):
|
|
1109
|
+
"""Set minimum and maximum z-levels (if not provided) based on the camera config."""
|
|
1110
|
+
if min_z is None:
|
|
1111
|
+
if min_h is not None:
|
|
1112
|
+
min_z = self.camera_config.h_to_z(min_h)
|
|
1113
|
+
min_z = np.maximum(min_z, self.z.min())
|
|
1114
|
+
if max_z is None:
|
|
1115
|
+
if max_h is not None:
|
|
1116
|
+
max_z = self.camera_config.h_to_z(max_h)
|
|
1117
|
+
max_z = np.minimum(max_z, self.z.max())
|
|
1118
|
+
if min_z and max_z:
|
|
1119
|
+
# check for z_min < z_max
|
|
1120
|
+
if min_z > max_z:
|
|
1121
|
+
raise ValueError("Minimum water level is higher than maximum water level.")
|
|
1122
|
+
return min_z, max_z
|
|
1123
|
+
|
|
1124
|
+
def _preprocess_l_range(self, l_min: float, l_max: float, ds_max=0.5, dz_max=0.02) -> List[float]:
|
|
1125
|
+
"""Generate a list of evaluation points between l_min and l_max for water level detection.
|
|
1126
|
+
|
|
1127
|
+
Controls on evaluation are that vertically (using `self.z`), points are at least `dz_max` meters apart and
|
|
1128
|
+
horizontally (using `self.s`), points are included if the distance between them exceeds `dz_min` meters.
|
|
1129
|
+
|
|
1130
|
+
Parameters
|
|
1131
|
+
----------
|
|
1132
|
+
l_min: float
|
|
1133
|
+
Minimum l value for evaluation.
|
|
1134
|
+
l_max: float
|
|
1135
|
+
Maximum l value for evaluation.
|
|
1136
|
+
ds_max : float, optional
|
|
1137
|
+
maximum step size between evaluation points in horizontal direction, by default 0.5.
|
|
1138
|
+
dz_max : float, optional
|
|
1139
|
+
maximum step size between evaluation points in vertical direction, by default 0.02.
|
|
1140
|
+
|
|
1141
|
+
Returns
|
|
1142
|
+
-------
|
|
1143
|
+
Tuple[List[float], List[float]]
|
|
1144
|
+
lists of l-coordinates and associated z-coordinates for evaluation.
|
|
1145
|
+
|
|
1146
|
+
"""
|
|
1147
|
+
# # Initial list of evaluation points
|
|
1148
|
+
# l_range = []
|
|
1149
|
+
# z_range = []
|
|
1150
|
+
# Start with the first point within the range
|
|
1151
|
+
current_l = l_min
|
|
1152
|
+
last_z = None
|
|
1153
|
+
last_s = None
|
|
1154
|
+
|
|
1155
|
+
# Add all points from self.l that lie within [l_min, l_max]
|
|
1156
|
+
valid_l_indices = (self.l >= l_min) & (self.l <= l_max) # Logical filter
|
|
1157
|
+
l_range = list(self.l[valid_l_indices])
|
|
1158
|
+
z_range = list(self.z[valid_l_indices])
|
|
1159
|
+
|
|
1160
|
+
# Iterate over the self.l values by interpolating within the range
|
|
1161
|
+
while current_l <= l_max:
|
|
1162
|
+
# Interpolate x, y, z, and s using the available properties
|
|
1163
|
+
# x = self.interp_x(current_l)
|
|
1164
|
+
# y = self.interp_y(current_l)
|
|
1165
|
+
z = self.interp_z(current_l)
|
|
1166
|
+
s = self.interp_s_from_l(current_l)
|
|
1167
|
+
|
|
1168
|
+
# Append the point if it satisfies the required criteria
|
|
1169
|
+
if last_z is None or last_s is None or abs(z - last_z) >= dz_max or abs(s - last_s) >= ds_max:
|
|
1170
|
+
l_range.append(current_l)
|
|
1171
|
+
z_range.append(z)
|
|
1172
|
+
last_z = z
|
|
1173
|
+
last_s = s
|
|
1174
|
+
|
|
1175
|
+
# Increment l
|
|
1176
|
+
current_l += 0.01 # Increment in steps small enough to meet any constraint
|
|
1177
|
+
|
|
1178
|
+
# add the final l
|
|
1179
|
+
if current_l > l_max:
|
|
1180
|
+
l_range.append(l_max)
|
|
1181
|
+
z_range.append(self.interp_z(l_max))
|
|
1182
|
+
|
|
1183
|
+
# sort l_range and z_range by incremental l_range
|
|
1184
|
+
sorted_indices = np.argsort(l_range)
|
|
1185
|
+
l_range = np.array(l_range)[sorted_indices]
|
|
1186
|
+
z_range = np.array(z_range)[sorted_indices]
|
|
1187
|
+
|
|
1188
|
+
return l_range, z_range
|
|
1189
|
+
|
|
1190
|
+
def _water_level_score_range(
|
|
1191
|
+
self,
|
|
1192
|
+
img: np.ndarray,
|
|
1193
|
+
bank: BANK_OPTIONS = "far",
|
|
1194
|
+
bin_size: int = 5,
|
|
1195
|
+
length: float = 2.0,
|
|
1196
|
+
padding: float = 0.5,
|
|
1197
|
+
offset: float = 0.0,
|
|
1198
|
+
ds_max: Optional[float] = 0.5,
|
|
1199
|
+
dz_max: Optional[float] = 0.02,
|
|
1200
|
+
min_h: Optional[float] = None,
|
|
1201
|
+
max_h: Optional[float] = None,
|
|
1202
|
+
min_z: Optional[float] = None,
|
|
1203
|
+
max_z: Optional[float] = None,
|
|
1204
|
+
) -> float:
|
|
1205
|
+
"""Evaluate a range of scores for water level detection.
|
|
1206
|
+
|
|
1207
|
+
This computes the histogram score for a range of l values and returns all scores for further evaluation.
|
|
1208
|
+
The histogram score is a measure of dissimilarity (low means very dissimilar) of pixel intensity distributions
|
|
1209
|
+
left and right of hypothesized water lines. The evaluation points along the cross section (l-values) are
|
|
1210
|
+
determined by two parameters max_ds (maximum step in horizontal direction) and max_dz (maximum step in vertical
|
|
1211
|
+
direction).
|
|
1212
|
+
|
|
1213
|
+
Parameters
|
|
1214
|
+
----------
|
|
1215
|
+
img : np.ndarray
|
|
1216
|
+
image (uint8) used to estimate water level from.
|
|
1217
|
+
bank: Literal["far", "near", "both"], optional
|
|
1218
|
+
select from which bank to detect the water level. Use this if camera is positioned in a way that only
|
|
1219
|
+
one shore is clearly distinguishable and not obscured. Typically you will use "far" if the camera is
|
|
1220
|
+
positioned on one bank, aimed perpendicular to the flow. Use "near" if not the full cross section is
|
|
1221
|
+
visible, but only the part nearest the camera. And leave empty when both banks are clearly visible and
|
|
1222
|
+
approximately the same in distance (e.g. middle of a bridge). If not provided, the bank is detected based
|
|
1223
|
+
on the best estimate from both banks.
|
|
1224
|
+
bin_size : int, optional
|
|
1225
|
+
Size of bins for histogram calculation of the provided image intensities, default 5.
|
|
1226
|
+
length : float, optional
|
|
1227
|
+
length of the waterline [m], by default 2.0
|
|
1228
|
+
padding : float, optional
|
|
1229
|
+
amount of distance [m] to extend the polygon beyond the waterline, by default 0.5. Two polygons are drawn
|
|
1230
|
+
left and right of hypothesized water line at -padding and +padding.
|
|
1231
|
+
offset : float, optional
|
|
1232
|
+
perpendicular offset of the waterline from the cross-section [m], by default 0.0
|
|
1233
|
+
ds_max : float, optional
|
|
1234
|
+
maximum step size between evaluation points in horizontal direction, by default 0.5.
|
|
1235
|
+
dz_max : float, optional
|
|
1236
|
+
maximum step size between evaluation points in vertical direction, by default 0.02.
|
|
1237
|
+
min_h : float, optional
|
|
1238
|
+
minimum water level to try detection [m]. If not provided, the minimum water level is taken from the
|
|
1239
|
+
cross section.
|
|
1240
|
+
max_h : float, optional
|
|
1241
|
+
maximum water level to try detection [m]. If not provided, the maximum water level is taken from the
|
|
1242
|
+
cross section.
|
|
1243
|
+
min_z : float, optional
|
|
1244
|
+
same as min_h but using z-coordinates instead of local datum, min_z overrules min_h
|
|
1245
|
+
max_z : float, optional
|
|
1246
|
+
same as max_z but using z-coordinates instead of local datum, max_z overrules max_h
|
|
1247
|
+
|
|
1248
|
+
"""
|
|
1249
|
+
l_min, l_max = self.get_line_of_interest(bank=bank)
|
|
1250
|
+
min_z, max_z = self._preprocess_level_range(min_h, max_h, min_z, max_z)
|
|
1251
|
+
l_range, z_range = self._preprocess_l_range(l_min=l_min, l_max=l_max, ds_max=ds_max, dz_max=dz_max)
|
|
1252
|
+
if len(img.shape) == 3:
|
|
1253
|
+
# flatten image first if it his a time dimension
|
|
1254
|
+
img = img.mean(axis=2)
|
|
1255
|
+
assert (
|
|
1256
|
+
img.shape[0] == self.camera_config.height
|
|
1257
|
+
), f"Image height {img.shape[0]} is not the same as camera_config height {self.camera_config.height}"
|
|
1258
|
+
assert (
|
|
1259
|
+
img.shape[1] == self.camera_config.width
|
|
1260
|
+
), f"Image width {img.shape[1]} is not the same as camera_config width {self.camera_config.width}"
|
|
1261
|
+
|
|
1262
|
+
# gridded results
|
|
1263
|
+
results = [
|
|
1264
|
+
self.get_histogram_score(
|
|
1265
|
+
x=[l],
|
|
1266
|
+
img=img,
|
|
1267
|
+
bin_size=bin_size,
|
|
1268
|
+
offset=offset,
|
|
1269
|
+
padding=padding,
|
|
1270
|
+
length=length,
|
|
1271
|
+
min_z=min_z,
|
|
1272
|
+
max_z=max_z,
|
|
1273
|
+
)
|
|
1274
|
+
for l in l_range
|
|
1275
|
+
]
|
|
1276
|
+
return l_range, z_range, results
|
|
1277
|
+
|
|
1077
1278
|
def detect_water_level(
|
|
1078
1279
|
self,
|
|
1079
1280
|
img: np.ndarray,
|
|
@@ -1087,11 +1288,11 @@ class CrossSection:
|
|
|
1087
1288
|
min_z: Optional[float] = None,
|
|
1088
1289
|
max_z: Optional[float] = None,
|
|
1089
1290
|
) -> float:
|
|
1090
|
-
"""Detect water level
|
|
1291
|
+
"""Detect water level from provided image through optimization.
|
|
1091
1292
|
|
|
1092
1293
|
Water level detection is done by first detecting the water line along the cross-section by comparisons
|
|
1093
1294
|
of distribution functions left and right of hypothesized water lines, and then looking up the water level
|
|
1094
|
-
associated with the water line location.
|
|
1295
|
+
associated with the water line location. A differential evolution optimization is used to find the optimum.
|
|
1095
1296
|
|
|
1096
1297
|
Parameters
|
|
1097
1298
|
----------
|
|
@@ -1124,19 +1325,14 @@ class CrossSection:
|
|
|
1124
1325
|
max_z : float, optional
|
|
1125
1326
|
same as max_z but using z-coordinates instead of local datum, max_z overrules max_h
|
|
1126
1327
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
min_z = np.maximum(min_z, self.z.min())
|
|
1132
|
-
if max_z is None:
|
|
1133
|
-
if max_h is not None:
|
|
1134
|
-
max_z = self.camera_config.h_to_z(max_h)
|
|
1135
|
-
max_z = np.minimum(max_z, self.z.max())
|
|
1136
|
-
if min_z and max_z:
|
|
1137
|
-
if min_z > max_z:
|
|
1138
|
-
raise ValueError("Minimum water level is higher than maximum water level.")
|
|
1328
|
+
Returns
|
|
1329
|
+
-------
|
|
1330
|
+
float
|
|
1331
|
+
Most likely water level, according to optimization and scoring (most distinct intensity PDF)
|
|
1139
1332
|
|
|
1333
|
+
"""
|
|
1334
|
+
l_min, l_max = self.get_line_of_interest(bank=bank)
|
|
1335
|
+
min_z, max_z = self._preprocess_level_range(min_h, max_h, min_z, max_z)
|
|
1140
1336
|
if len(img.shape) == 3:
|
|
1141
1337
|
# flatten image first if it his a time dimension
|
|
1142
1338
|
img = img.mean(axis=2)
|
|
@@ -1146,9 +1342,7 @@ class CrossSection:
|
|
|
1146
1342
|
assert (
|
|
1147
1343
|
img.shape[1] == self.camera_config.width
|
|
1148
1344
|
), f"Image width {img.shape[1]} is not the same as camera_config width {self.camera_config.width}"
|
|
1149
|
-
|
|
1150
|
-
# import pdb;pdb.set_trace()
|
|
1151
|
-
l_min, l_max = self.get_line_of_interest(bank=bank)
|
|
1345
|
+
|
|
1152
1346
|
opt = differential_evolution(
|
|
1153
1347
|
self.get_histogram_score,
|
|
1154
1348
|
popsize=50,
|
|
@@ -1167,3 +1361,93 @@ class CrossSection:
|
|
|
1167
1361
|
stacklevel=2,
|
|
1168
1362
|
)
|
|
1169
1363
|
return h
|
|
1364
|
+
|
|
1365
|
+
def detect_water_level_s2n(
|
|
1366
|
+
self,
|
|
1367
|
+
img: np.ndarray,
|
|
1368
|
+
bank: BANK_OPTIONS = "far",
|
|
1369
|
+
bin_size: int = 5,
|
|
1370
|
+
length: float = 2.0,
|
|
1371
|
+
padding: float = 0.5,
|
|
1372
|
+
offset: float = 0.0,
|
|
1373
|
+
ds_max: Optional[float] = 0.5,
|
|
1374
|
+
dz_max: Optional[float] = 0.02,
|
|
1375
|
+
min_h: Optional[float] = None,
|
|
1376
|
+
max_h: Optional[float] = None,
|
|
1377
|
+
min_z: Optional[float] = None,
|
|
1378
|
+
max_z: Optional[float] = None,
|
|
1379
|
+
) -> float:
|
|
1380
|
+
"""Detect water level optically from provided image, through evaluation of a vector of locations.
|
|
1381
|
+
|
|
1382
|
+
Water level detection is done by first detecting the water line along the cross-section by comparisons
|
|
1383
|
+
of distribution functions left and right of hypothesized water lines, and then looking up the water level
|
|
1384
|
+
associated with the water line location. Because a full vector of results is evaluated, the signal to noise
|
|
1385
|
+
ratio can be evaluated and used to understand the quality of the water level detection. Two parameters are used
|
|
1386
|
+
to control the spacing between l (location) coordinates: `ds_max` controls the maximum step size in horizontal
|
|
1387
|
+
direction, `dz_max` controls the maximum step size in vertical direction.
|
|
1388
|
+
|
|
1389
|
+
Parameters
|
|
1390
|
+
----------
|
|
1391
|
+
img : np.ndarray
|
|
1392
|
+
image (uint8) used to estimate water level from.
|
|
1393
|
+
bank: Literal["far", "near", "both"], optional
|
|
1394
|
+
select from which bank to detect the water level. Use this if camera is positioned in a way that only
|
|
1395
|
+
one shore is clearly distinguishable and not obscured. Typically you will use "far" if the camera is
|
|
1396
|
+
positioned on one bank, aimed perpendicular to the flow. Use "near" if not the full cross section is
|
|
1397
|
+
visible, but only the part nearest the camera. And leave empty when both banks are clearly visible and
|
|
1398
|
+
approximately the same in distance (e.g. middle of a bridge). If not provided, the bank is detected based
|
|
1399
|
+
on the best estimate from both banks.
|
|
1400
|
+
bin_size : int, optional
|
|
1401
|
+
Size of bins for histogram calculation of the provided image intensities, default 5.
|
|
1402
|
+
length : float, optional
|
|
1403
|
+
length of the waterline [m], by default 2.0.
|
|
1404
|
+
padding : float, optional
|
|
1405
|
+
amount of distance [m] to extend the polygon beyond the waterline, by default 0.5. Two polygons are drawn
|
|
1406
|
+
left and right of hypothesized water line at -padding and +padding.
|
|
1407
|
+
offset : float, optional
|
|
1408
|
+
perpendicular offset of the waterline from the cross-section [m], by default 0.0.
|
|
1409
|
+
ds_max : float, optional
|
|
1410
|
+
maximum step size between evaluation points in horizontal direction, by default 0.5.
|
|
1411
|
+
dz_max : float, optional
|
|
1412
|
+
maximum step size between evaluation points in vertical direction, by default 0.02.
|
|
1413
|
+
min_h : float, optional
|
|
1414
|
+
minimum water level to try detection [m]. If not provided, the minimum water level is taken from the
|
|
1415
|
+
cross section.
|
|
1416
|
+
max_h : float, optional
|
|
1417
|
+
maximum water level to try detection [m]. If not provided, the maximum water level is taken from the
|
|
1418
|
+
cross section.
|
|
1419
|
+
min_z : float, optional
|
|
1420
|
+
same as min_h but using z-coordinates instead of local datum, min_z overrules min_h.
|
|
1421
|
+
max_z : float, optional
|
|
1422
|
+
same as max_z but using z-coordinates instead of local datum, max_z overrules max_h.
|
|
1423
|
+
|
|
1424
|
+
Returns
|
|
1425
|
+
-------
|
|
1426
|
+
float
|
|
1427
|
+
Most likely water level, according to optimization and scoring (most distinct intensity PDF).
|
|
1428
|
+
float
|
|
1429
|
+
Signal to noise ratio, calculated as the mean of all computed scores divided by the optimum (lowest) score
|
|
1430
|
+
found in the entire l-range.
|
|
1431
|
+
|
|
1432
|
+
"""
|
|
1433
|
+
l_range, z_range, results = self._water_level_score_range(
|
|
1434
|
+
img=img,
|
|
1435
|
+
bank=bank,
|
|
1436
|
+
bin_size=bin_size,
|
|
1437
|
+
length=length,
|
|
1438
|
+
padding=padding,
|
|
1439
|
+
offset=offset,
|
|
1440
|
+
ds_max=ds_max,
|
|
1441
|
+
dz_max=dz_max,
|
|
1442
|
+
min_h=min_h,
|
|
1443
|
+
max_h=max_h,
|
|
1444
|
+
min_z=min_z,
|
|
1445
|
+
max_z=max_z,
|
|
1446
|
+
)
|
|
1447
|
+
# find optimum (minimum score)
|
|
1448
|
+
idx = np.argmin(results)
|
|
1449
|
+
s2n = np.mean(results) / results[idx]
|
|
1450
|
+
z = z_range[idx]
|
|
1451
|
+
h = self.camera_config.z_to_h(z)
|
|
1452
|
+
# warning if the optimum is on the edge of the search space for l
|
|
1453
|
+
return h, s2n
|
pyorc/api/mask.py
CHANGED
|
@@ -10,12 +10,11 @@ from pyorc import helpers
|
|
|
10
10
|
from pyorc.const import corr, s2n, v_x, v_y
|
|
11
11
|
|
|
12
12
|
commondoc = """
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
Returns
|
|
14
|
+
-------
|
|
15
|
+
mask : xr.DataArray
|
|
16
|
+
mask applicable to input dataset with ``ds.velocimetry.filter(mask)``.
|
|
17
|
+
If ``inplace=True``, the dataset will be returned masked with ``mask``.
|
|
19
18
|
"""
|
|
20
19
|
|
|
21
20
|
|
|
@@ -203,7 +202,7 @@ class _Velocimetry_MaskMethods:
|
|
|
203
202
|
|
|
204
203
|
@_base_mask(time_allowed=True)
|
|
205
204
|
def corr(self, tolerance=0.1):
|
|
206
|
-
"""
|
|
205
|
+
"""Mask values with too low correlation.
|
|
207
206
|
|
|
208
207
|
Parameters
|
|
209
208
|
----------
|
|
@@ -304,16 +303,17 @@ class _Velocimetry_MaskMethods:
|
|
|
304
303
|
(default: 1) wdw is used to fill wdw_x_min and wdwd_y_min with its negative (-wdw) value, and wdw_y_min and
|
|
305
304
|
kwargs : dict
|
|
306
305
|
keyword arguments to pass to ``helpers.stack_window``. These can be:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
306
|
+
|
|
307
|
+
- ``wdw_x_min`` : int, optional
|
|
308
|
+
window size in negative x-direction of grid (must be negative), overrules wdw in negative x-direction if
|
|
309
|
+
set.
|
|
310
|
+
- ``wdw_x_max`` : int, optional
|
|
311
|
+
window size in positive x-direction of grid, overrules wdw in positive x-direction if set.
|
|
312
|
+
- ``wdw_y_min`` : int, optional
|
|
313
|
+
window size in negative y-direction of grid (must be negative), overrules wdw in negative y-direction if
|
|
314
|
+
set.
|
|
315
|
+
- ``wdw_y_max`` : int, optional
|
|
316
|
+
window size in positive y-direction of grid, overrules wdw in positive x-direction if set.
|
|
317
317
|
|
|
318
318
|
"""
|
|
319
319
|
# collect points within a stride, collate and analyze for nan fraction
|
|
@@ -326,7 +326,7 @@ class _Velocimetry_MaskMethods:
|
|
|
326
326
|
def window_mean(self, tolerance=0.7, wdw=1, mode="or", **kwargs):
|
|
327
327
|
"""Mask values when their value deviates significantly from mean.
|
|
328
328
|
|
|
329
|
-
This is computed as relative fraction from the mean of its
|
|
329
|
+
This is computed as relative fraction from the mean of its neighbours (inc. itself).
|
|
330
330
|
|
|
331
331
|
Parameters
|
|
332
332
|
----------
|
|
@@ -339,16 +339,17 @@ class _Velocimetry_MaskMethods:
|
|
|
339
339
|
be within tolerance.
|
|
340
340
|
kwargs : dict
|
|
341
341
|
keyword arguments to pass to ``helpers.stack_window``. These can be:
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
342
|
+
|
|
343
|
+
- ``wdw_x_min`` : int, optional
|
|
344
|
+
window size in negative x-direction of grid (must be negative), overrules wdw in negative x-direction
|
|
345
|
+
if set.
|
|
346
|
+
- ``wdw_x_max`` : int, optional
|
|
347
|
+
window size in positive x-direction of grid, overrules wdw in positive x-direction if set.
|
|
348
|
+
- ``wdw_y_min`` : int, optional
|
|
349
|
+
window size in negative y-direction of grid (must be negative), overrules wdw in negative y-direction
|
|
350
|
+
if set.
|
|
351
|
+
- ``wdw_y_max`` : int, optional
|
|
352
|
+
window size in positive y-direction of grid, overrules wdw in positive x-direction if set.
|
|
352
353
|
|
|
353
354
|
"""
|
|
354
355
|
# collect points within a stride, collate and analyze for median value and deviation
|
pyorc/api/plot.py
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
4
|
import functools
|
|
5
|
+
import warnings
|
|
5
6
|
|
|
6
7
|
import matplotlib.pyplot as plt
|
|
7
8
|
import matplotlib.ticker as mticker
|
|
8
9
|
import numpy as np
|
|
9
|
-
import warnings
|
|
10
|
-
|
|
11
10
|
from matplotlib import colors, patheffects
|
|
12
11
|
|
|
13
12
|
from pyorc import helpers
|
|
@@ -207,10 +206,9 @@ def _base_plot(plot_func):
|
|
|
207
206
|
color="r",
|
|
208
207
|
label="water level",
|
|
209
208
|
)
|
|
210
|
-
except:
|
|
209
|
+
except Exception:
|
|
211
210
|
warnings.warn(
|
|
212
|
-
"Not able to find a unique location for plotting of water level",
|
|
213
|
-
stacklevel=2
|
|
211
|
+
"Not able to find a unique location for plotting of water level", stacklevel=2
|
|
214
212
|
)
|
|
215
213
|
|
|
216
214
|
# draw some depth lines for better visual interpretation.
|
|
@@ -620,7 +618,7 @@ def set_default_kwargs(kwargs, method="quiver", mode="local"):
|
|
|
620
618
|
kwargs["cmap"] = "rainbow" # the famous rainbow colormap!
|
|
621
619
|
if "vmin" not in kwargs and "vmax" not in kwargs and "norm" not in kwargs:
|
|
622
620
|
# set a normalization array
|
|
623
|
-
norm = [0, 0.05, 0.1, 0.2, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
|
|
621
|
+
norm = [0, 0.05, 0.1, 0.2, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 5.0, 10.0]
|
|
624
622
|
kwargs["norm"] = colors.BoundaryNorm(norm, ncolors=256, extend="max")
|
|
625
623
|
if method == "quiver":
|
|
626
624
|
if "scale" not in kwargs:
|
|
@@ -752,8 +750,16 @@ def plot_text(ax, ds, prefix, suffix):
|
|
|
752
750
|
yloc = 0.95
|
|
753
751
|
_ds.transect.get_river_flow(q_name="q")
|
|
754
752
|
Q = np.abs(_ds.river_flow)
|
|
753
|
+
v_surf = _ds.transect.get_v_surf()
|
|
754
|
+
v_bulk = _ds.transect.get_v_bulk()
|
|
755
755
|
string = prefix
|
|
756
|
-
string +=
|
|
756
|
+
string += (
|
|
757
|
+
f"$h_a$: {_ds.transect.h_a:1.2f} m | "
|
|
758
|
+
f"$v_{{surf}}$: {v_surf.values:1.2f} m/s | "
|
|
759
|
+
f"$\\overline{{v}}$: {v_bulk.values:1.2f} m/s\n"
|
|
760
|
+
f"$Q$: {Q.values:1.2f} m3/s"
|
|
761
|
+
)
|
|
762
|
+
|
|
757
763
|
if "q_nofill" in ds:
|
|
758
764
|
_ds.transect.get_river_flow(q_name="q_nofill")
|
|
759
765
|
Q_nofill = np.abs(_ds.river_flow)
|
|
@@ -765,7 +771,7 @@ def plot_text(ax, ds, prefix, suffix):
|
|
|
765
771
|
xloc,
|
|
766
772
|
yloc,
|
|
767
773
|
string,
|
|
768
|
-
size=
|
|
774
|
+
size=18,
|
|
769
775
|
horizontalalignment="right",
|
|
770
776
|
verticalalignment="top",
|
|
771
777
|
path_effects=path_effects,
|
pyorc/api/transect.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
import xarray as xr
|
|
5
|
+
from shapely import geometry
|
|
5
6
|
from xarray.core import utils
|
|
6
7
|
|
|
7
8
|
from pyorc import helpers
|
|
@@ -34,6 +35,26 @@ class Transect(ORCBase):
|
|
|
34
35
|
coords = [[_x, _y, _z] for _x, _y, _z in zip(self._obj.xcoords, self._obj.ycoords, self._obj.zcoords)]
|
|
35
36
|
return CrossSection(camera_config=self.camera_config, cross_section=coords)
|
|
36
37
|
|
|
38
|
+
@property
|
|
39
|
+
def wetted_surface_polygon(self) -> geometry.MultiPolygon:
|
|
40
|
+
"""Return wetted surface as `shapely.geometry.MultiPolygon` object."""
|
|
41
|
+
return self.cross_section.get_wetted_surface_sz(self.h_a)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def wetted_perimeter_linestring(self) -> geometry.MultiLineString:
|
|
45
|
+
"""Return wetted perimeter as `shapely.geometry.MultiLineString` object."""
|
|
46
|
+
return self.cross_section.get_wetted_surface_sz(self.h_a, perimeter=True)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def wetted_surface(self) -> float:
|
|
50
|
+
"""Return wetted surface as float."""
|
|
51
|
+
return self.wetted_surface_polygon.area
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def wetted_perimeter(self) -> float:
|
|
55
|
+
"""Return wetted perimeter as float."""
|
|
56
|
+
return self.wetted_perimeter_linestring.length
|
|
57
|
+
|
|
37
58
|
def vector_to_scalar(self, v_x="v_x", v_y="v_y"):
|
|
38
59
|
"""Set "v_eff" and "v_dir" variables as effective velocities over cross-section, and its angle.
|
|
39
60
|
|
|
@@ -129,30 +150,6 @@ class Transect(ORCBase):
|
|
|
129
150
|
points_proj = self.camera_config.project_points(points, within_image=within_image, swap_y_coords=True)
|
|
130
151
|
return points_proj
|
|
131
152
|
|
|
132
|
-
def get_wetted_perspective(self, h, sample_size=1000):
|
|
133
|
-
"""Get wetted polygon in camera perspective.
|
|
134
|
-
|
|
135
|
-
Parameters
|
|
136
|
-
----------
|
|
137
|
-
h : float
|
|
138
|
-
The water level value to calculate the surface perspective.
|
|
139
|
-
sample_size : int, optional
|
|
140
|
-
The number of points to densify the transect with, by default 1000
|
|
141
|
-
|
|
142
|
-
Returns
|
|
143
|
-
-------
|
|
144
|
-
ndarray
|
|
145
|
-
A numpy array containing the points forming the wetted polygon perspective.
|
|
146
|
-
|
|
147
|
-
"""
|
|
148
|
-
bottom_points, surface_points = self.get_bottom_surface_z_perspective(h=h, sample_size=sample_size)
|
|
149
|
-
# concatenate points reversing one set for preps of a polygon
|
|
150
|
-
pol_points = np.concatenate([bottom_points, np.flipud(surface_points)], axis=0)
|
|
151
|
-
|
|
152
|
-
# add the first point at the end to close the polygon
|
|
153
|
-
pol_points = np.concatenate([pol_points, pol_points[0:1]], axis=0)
|
|
154
|
-
return pol_points
|
|
155
|
-
|
|
156
153
|
def get_depth_perspective(self, h, sample_size=1000, interval=25):
|
|
157
154
|
"""Get line (x, y) pairs that show the depth over several intervals in the wetted part of the cross section.
|
|
158
155
|
|
|
@@ -177,64 +174,59 @@ class Transect(ORCBase):
|
|
|
177
174
|
# make line pairs
|
|
178
175
|
return list(zip(bottom_points, surface_points))
|
|
179
176
|
|
|
180
|
-
def
|
|
181
|
-
"""
|
|
177
|
+
def get_v_surf(self, v_name="v_eff"):
|
|
178
|
+
"""Compute mean surface velocity in locations that are below water level.
|
|
182
179
|
|
|
183
180
|
Parameters
|
|
184
181
|
----------
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
xs : np.array, optional
|
|
188
|
-
x-coordinates to transform, derived from self.x if not provided (Default value = None)
|
|
189
|
-
ys :
|
|
190
|
-
y-coordinates to transform, derived from self.y if not provided (Default value = None)
|
|
191
|
-
mask_outside :
|
|
192
|
-
values not fitting in the original camera frame are set to NaN (Default value = True)
|
|
182
|
+
v_name : str, optional
|
|
183
|
+
name of variable where surface velocities [m s-1] are stored (Default value = "v_eff")
|
|
193
184
|
|
|
194
185
|
Returns
|
|
195
186
|
-------
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
rows : list of ints
|
|
199
|
-
rows of locations in original camera perspective
|
|
200
|
-
|
|
187
|
+
xr.DataArray
|
|
188
|
+
mean surface velocities for all provided quantiles or time steps
|
|
201
189
|
|
|
202
190
|
"""
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
#
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if
|
|
215
|
-
|
|
191
|
+
## Mean velocity over entire profile
|
|
192
|
+
z_a = self.camera_config.h_to_z(self.h_a)
|
|
193
|
+
|
|
194
|
+
depth = z_a - self._obj.zcoords
|
|
195
|
+
depth[depth < 0] = 0.0
|
|
196
|
+
|
|
197
|
+
# ds.transect.camera_config.get_depth(ds.zcoords, ds.transect.h_a)
|
|
198
|
+
wet_scoords = self._obj.scoords[depth > 0].values
|
|
199
|
+
if len(wet_scoords) == 0:
|
|
200
|
+
# no wet points found. Velocity can only be missing
|
|
201
|
+
v_av = np.nan
|
|
202
|
+
if len(wet_scoords) > 1:
|
|
203
|
+
velocity_int = self._obj[v_name].fillna(0.0).integrate(coord="scoords") # m2/s
|
|
204
|
+
width = (wet_scoords[-1] + (wet_scoords[-1] - wet_scoords[-2]) * 0.5) - (
|
|
205
|
+
wet_scoords[0] - (wet_scoords[1] - wet_scoords[0]) * 0.5
|
|
206
|
+
)
|
|
207
|
+
v_av = velocity_int / width
|
|
216
208
|
else:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
]
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return
|
|
209
|
+
v_av = self._obj[v_name][:, depth > 0]
|
|
210
|
+
return v_av
|
|
211
|
+
|
|
212
|
+
def get_v_bulk(self, q_name="q"):
|
|
213
|
+
"""Compute the bulk velocity.
|
|
214
|
+
|
|
215
|
+
Parameters
|
|
216
|
+
----------
|
|
217
|
+
q_name : str, optional
|
|
218
|
+
name of variable where depth integrated velocities [m2 s-1] are stored (Default value = "q")
|
|
219
|
+
|
|
220
|
+
Returns
|
|
221
|
+
-------
|
|
222
|
+
xr.DataArray
|
|
223
|
+
bulk velocities for all provided quantiles or time steps
|
|
224
|
+
|
|
225
|
+
"""
|
|
226
|
+
discharge = self._obj[q_name].fillna(0.0).integrate(coord="scoords")
|
|
227
|
+
wet_surf = self.wetted_surface
|
|
228
|
+
v_bulk = discharge / wet_surf
|
|
229
|
+
return v_bulk
|
|
238
230
|
|
|
239
231
|
def get_river_flow(self, q_name="q", discharge_name="river_flow"):
|
|
240
232
|
"""Integrate time series of depth averaged velocities [m2 s-1] into cross-section integrated flow [m3 s-1].
|
pyorc/cv.py
CHANGED
|
@@ -7,6 +7,7 @@ import warnings
|
|
|
7
7
|
import cv2
|
|
8
8
|
import numpy as np
|
|
9
9
|
import rasterio
|
|
10
|
+
from numba import jit, prange
|
|
10
11
|
from scipy import optimize
|
|
11
12
|
from shapely.affinity import rotate
|
|
12
13
|
from shapely.geometry import LineString, Point, Polygon
|
|
@@ -1034,16 +1035,48 @@ def get_aoi(dst_corners, resolution=None, method="corners"):
|
|
|
1034
1035
|
return bbox
|
|
1035
1036
|
|
|
1036
1037
|
|
|
1038
|
+
@jit(nopython=True, cache=True, nogil=True)
|
|
1039
|
+
def numba_extract_pixels(img, mask):
|
|
1040
|
+
"""Optimized pixel extraction within a polygon-defined mask."""
|
|
1041
|
+
pixel_values = []
|
|
1042
|
+
rows, cols = img.shape
|
|
1043
|
+
for i in prange(rows):
|
|
1044
|
+
for j in prange(cols):
|
|
1045
|
+
if mask[i, j] == 255:
|
|
1046
|
+
pixel_values.append(img[i, j])
|
|
1047
|
+
return np.array(pixel_values)
|
|
1048
|
+
|
|
1049
|
+
|
|
1037
1050
|
def get_polygon_pixels(img, pol, reverse_y=False):
|
|
1038
1051
|
"""Get pixel intensities within a polygon."""
|
|
1039
1052
|
if pol.is_empty:
|
|
1040
1053
|
return np.array([np.nan])
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1054
|
+
min_x, min_y, max_x, max_y = map(int, pol.bounds)
|
|
1055
|
+
# Ensure bounding box remains within the image dimensions
|
|
1056
|
+
min_x = max(min_x, 0)
|
|
1057
|
+
min_y = max(min_y, 0)
|
|
1058
|
+
max_x = min(max_x, img.shape[1])
|
|
1059
|
+
max_y = min(max_y, img.shape[0])
|
|
1060
|
+
|
|
1044
1061
|
if reverse_y:
|
|
1045
|
-
|
|
1046
|
-
|
|
1062
|
+
img = np.flipud(img)
|
|
1063
|
+
|
|
1064
|
+
# Crop the image based on the bounding box
|
|
1065
|
+
cropped_img = img[min_y:max_y, min_x:max_x]
|
|
1066
|
+
# reduce polygon coordinates to ensure compatibility with cropped_img
|
|
1067
|
+
cropped_polygon_coords = [(x - min_x, y - min_y) for x, y in pol.exterior.coords]
|
|
1068
|
+
|
|
1069
|
+
mask = np.zeros_like(cropped_img, dtype=np.uint8)
|
|
1070
|
+
if 0 in mask.shape:
|
|
1071
|
+
# no shape in mask, so return empty array instantly
|
|
1072
|
+
return np.array([], dtype=np.uint8)
|
|
1073
|
+
try:
|
|
1074
|
+
cv2.fillPoly(mask, [np.array(cropped_polygon_coords, dtype=np.int32)], color=255)
|
|
1075
|
+
except Exception:
|
|
1076
|
+
import pdb
|
|
1077
|
+
|
|
1078
|
+
pdb.set_trace()
|
|
1079
|
+
return numba_extract_pixels(cropped_img, mask)
|
|
1047
1080
|
|
|
1048
1081
|
|
|
1049
1082
|
def optimize_intrinsic(src, dst, height, width, c=2.0, lens_position=None, camera_matrix=None, dist_coeffs=None):
|
pyorc/service/velocimetry.py
CHANGED
|
@@ -31,29 +31,46 @@ def get_water_level(
|
|
|
31
31
|
n_start: int = 0,
|
|
32
32
|
n_end: int = 1,
|
|
33
33
|
method: const.ALLOWED_COLOR_METHODS_WATER_LEVEL = "grayscale",
|
|
34
|
+
s2n_thres: float = 3.0,
|
|
34
35
|
frames_options: Optional[Dict] = None,
|
|
35
36
|
water_level_options: Optional[Dict] = None,
|
|
37
|
+
logger: logging.Logger = logger,
|
|
36
38
|
):
|
|
37
39
|
water_level_options = {} if water_level_options is None else water_level_options
|
|
38
40
|
frames_options = {} if frames_options is None else frames_options
|
|
39
|
-
|
|
40
|
-
if
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
41
|
+
# if frames_options is list of dict then continue, if not then make list of dict
|
|
42
|
+
if not isinstance(frames_options, list):
|
|
43
|
+
frames_options = [frames_options]
|
|
44
|
+
for frames_options_ in frames_options:
|
|
45
|
+
# get method from frames_options if available, otherwise use the parent or default one
|
|
46
|
+
method_ = frames_options_.pop("method", method)
|
|
47
|
+
s2n_thres_ = frames_options_.pop("s2n_thres", s2n_thres)
|
|
48
|
+
|
|
49
|
+
if method_ not in ["grayscale", "hue", "sat", "val"]:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"Method {method} not supported for water level detection, choose one"
|
|
52
|
+
f" of {const.ALLOWED_COLOR_METHODS_WATER_LEVEL}"
|
|
53
|
+
)
|
|
54
|
+
da_frames = video.get_frames(method=method_)[n_start:n_end]
|
|
55
|
+
# preprocess
|
|
56
|
+
logger.debug(f"Applying preprocessing methods {frames_options_}")
|
|
57
|
+
da_frames = apply_methods(da_frames, "frames", logger=logger, skip_args=["to_video"], **frames_options_)
|
|
58
|
+
# if preprocessing still results in a time dim, average in time
|
|
59
|
+
if "time" in da_frames.dims:
|
|
60
|
+
da_mean = da_frames.mean(dim="time")
|
|
61
|
+
else:
|
|
62
|
+
da_mean = da_frames
|
|
63
|
+
# extract the image
|
|
64
|
+
img = np.uint8(da_mean.values)
|
|
65
|
+
h_a, s2n = cross_section.detect_water_level_s2n(img, **water_level_options)
|
|
66
|
+
if s2n > s2n_thres_:
|
|
67
|
+
# high enough signal-to-noise ratio, so return, otherwise continue with next frame treatment set
|
|
68
|
+
logger.debug(f"Found significant water level at h: {h_a:.3f} m with signal-to-noise: {s2n} > {s2n_thres_}")
|
|
69
|
+
return h_a
|
|
70
|
+
else:
|
|
71
|
+
logger.debug(f"Found water level at h: {h_a:.3f} m with too low signal-to-noise: {s2n} < {s2n_thres_}")
|
|
72
|
+
# if none of frame treatments gives a satisfactory h_a, return None
|
|
73
|
+
return
|
|
57
74
|
|
|
58
75
|
|
|
59
76
|
def vmin_vmax_to_norm(opts):
|
|
@@ -434,7 +451,8 @@ class VelocityFlowProcessor(object):
|
|
|
434
451
|
def water_level(self, **kwargs):
|
|
435
452
|
try:
|
|
436
453
|
self.logger.debug("Estimating water level from video by crossing water line with cross section.")
|
|
437
|
-
|
|
454
|
+
|
|
455
|
+
h_a = get_water_level(self.video_obj, cross_section=self.cross_section_wl, logger=self.logger, **kwargs)
|
|
438
456
|
if h_a is None:
|
|
439
457
|
self.logger.error("Water level could not be estimated from video. Please set a water level with --h_a.")
|
|
440
458
|
raise click.Abort()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|