pyopenrivercam 0.8.11__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.11.dist-info → pyopenrivercam-0.8.12.dist-info}/METADATA +1 -1
- {pyopenrivercam-0.8.11.dist-info → pyopenrivercam-0.8.12.dist-info}/RECORD +11 -11
- pyorc/__init__.py +1 -1
- pyorc/api/cross_section.py +280 -19
- pyorc/api/mask.py +29 -28
- pyorc/api/plot.py +11 -7
- pyorc/cv.py +38 -5
- pyorc/service/velocimetry.py +37 -19
- {pyopenrivercam-0.8.11.dist-info → pyopenrivercam-0.8.12.dist-info}/WHEEL +0 -0
- {pyopenrivercam-0.8.11.dist-info → pyopenrivercam-0.8.12.dist-info}/entry_points.txt +0 -0
- {pyopenrivercam-0.8.11.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,11 +8,11 @@ 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=
|
|
15
|
+
pyorc/api/plot.py,sha256=MxIEIS8l46bUaca0GtMazx8-k2_TbfQLrPCPAjuWos8,31082
|
|
16
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
|
|
@@ -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
|
@@ -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,7 +613,9 @@ 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(
|
|
616
|
+
def get_wetted_surface_sz(
|
|
617
|
+
self, h: float, perimeter: bool = False
|
|
618
|
+
) -> Union[geometry.MultiPolygon, geometry.MultiLineString]:
|
|
617
619
|
"""Retrieve a wetted surface or perimeter perpendicular to flow direction (SZ) for a water level.
|
|
618
620
|
|
|
619
621
|
This returns a `geometry.MultiPolygon` when a surface is requested (`perimeter=False`), and
|
|
@@ -1097,6 +1099,182 @@ class CrossSection:
|
|
|
1097
1099
|
)
|
|
1098
1100
|
return ax
|
|
1099
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
|
+
|
|
1100
1278
|
def detect_water_level(
|
|
1101
1279
|
self,
|
|
1102
1280
|
img: np.ndarray,
|
|
@@ -1110,11 +1288,11 @@ class CrossSection:
|
|
|
1110
1288
|
min_z: Optional[float] = None,
|
|
1111
1289
|
max_z: Optional[float] = None,
|
|
1112
1290
|
) -> float:
|
|
1113
|
-
"""Detect water level
|
|
1291
|
+
"""Detect water level from provided image through optimization.
|
|
1114
1292
|
|
|
1115
1293
|
Water level detection is done by first detecting the water line along the cross-section by comparisons
|
|
1116
1294
|
of distribution functions left and right of hypothesized water lines, and then looking up the water level
|
|
1117
|
-
associated with the water line location.
|
|
1295
|
+
associated with the water line location. A differential evolution optimization is used to find the optimum.
|
|
1118
1296
|
|
|
1119
1297
|
Parameters
|
|
1120
1298
|
----------
|
|
@@ -1147,19 +1325,14 @@ class CrossSection:
|
|
|
1147
1325
|
max_z : float, optional
|
|
1148
1326
|
same as max_z but using z-coordinates instead of local datum, max_z overrules max_h
|
|
1149
1327
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
min_z = np.maximum(min_z, self.z.min())
|
|
1155
|
-
if max_z is None:
|
|
1156
|
-
if max_h is not None:
|
|
1157
|
-
max_z = self.camera_config.h_to_z(max_h)
|
|
1158
|
-
max_z = np.minimum(max_z, self.z.max())
|
|
1159
|
-
if min_z and max_z:
|
|
1160
|
-
if min_z > max_z:
|
|
1161
|
-
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)
|
|
1162
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)
|
|
1163
1336
|
if len(img.shape) == 3:
|
|
1164
1337
|
# flatten image first if it his a time dimension
|
|
1165
1338
|
img = img.mean(axis=2)
|
|
@@ -1169,9 +1342,7 @@ class CrossSection:
|
|
|
1169
1342
|
assert (
|
|
1170
1343
|
img.shape[1] == self.camera_config.width
|
|
1171
1344
|
), f"Image width {img.shape[1]} is not the same as camera_config width {self.camera_config.width}"
|
|
1172
|
-
|
|
1173
|
-
# import pdb;pdb.set_trace()
|
|
1174
|
-
l_min, l_max = self.get_line_of_interest(bank=bank)
|
|
1345
|
+
|
|
1175
1346
|
opt = differential_evolution(
|
|
1176
1347
|
self.get_histogram_score,
|
|
1177
1348
|
popsize=50,
|
|
@@ -1190,3 +1361,93 @@ class CrossSection:
|
|
|
1190
1361
|
stacklevel=2,
|
|
1191
1362
|
)
|
|
1192
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:
|
|
@@ -755,7 +753,13 @@ def plot_text(ax, ds, prefix, suffix):
|
|
|
755
753
|
v_surf = _ds.transect.get_v_surf()
|
|
756
754
|
v_bulk = _ds.transect.get_v_bulk()
|
|
757
755
|
string = prefix
|
|
758
|
-
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
|
+
|
|
759
763
|
if "q_nofill" in ds:
|
|
760
764
|
_ds.transect.get_river_flow(q_name="q_nofill")
|
|
761
765
|
Q_nofill = np.abs(_ds.river_flow)
|
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
|