pyopenrivercam 0.8.11__tar.gz → 0.8.12__tar.gz

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.
Files changed (40) hide show
  1. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/CHANGELOG.md +17 -1
  2. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/PKG-INFO +1 -1
  3. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/__init__.py +1 -1
  4. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/api/cross_section.py +280 -19
  5. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/api/mask.py +29 -28
  6. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/api/plot.py +11 -7
  7. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/cv.py +38 -5
  8. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/service/velocimetry.py +37 -19
  9. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/.gitignore +0 -0
  10. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/.pre-commit-config.yaml +0 -0
  11. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/Dockerfile +0 -0
  12. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/LICENSE +0 -0
  13. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/README.md +0 -0
  14. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/TRADEMARK.md +0 -0
  15. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/environment.yml +0 -0
  16. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/api/__init__.py +0 -0
  17. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/api/cameraconfig.py +0 -0
  18. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/api/frames.py +0 -0
  19. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/api/orcbase.py +0 -0
  20. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/api/transect.py +0 -0
  21. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/api/velocimetry.py +0 -0
  22. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/api/video.py +0 -0
  23. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/cli/__init__.py +0 -0
  24. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/cli/cli_elements.py +0 -0
  25. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/cli/cli_utils.py +0 -0
  26. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/cli/log.py +0 -0
  27. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/cli/main.py +0 -0
  28. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/const.py +0 -0
  29. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/helpers.py +0 -0
  30. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/plot_helpers.py +0 -0
  31. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/project.py +0 -0
  32. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/pyorc.sh +0 -0
  33. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/sample_data.py +0 -0
  34. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/service/__init__.py +0 -0
  35. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/service/camera_config.py +0 -0
  36. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/velocimetry/__init__.py +0 -0
  37. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/velocimetry/ffpiv.py +0 -0
  38. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyorc/velocimetry/openpiv.py +0 -0
  39. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/pyproject.toml +0 -0
  40. {pyopenrivercam-0.8.11 → pyopenrivercam-0.8.12}/sonar-project.properties +0 -0
@@ -1,9 +1,25 @@
1
+ ## [0.8.12] = 2025-11-13
2
+ ### Added
3
+ - Added `CrossSection.detect_water_level_s2n` for signal to noise ratios.
4
+ ### Changed
5
+ - The CLI recipe .yml can now handle lists in `water_level.frames_options` and usage of a signal to noise acceptance
6
+ threshold in each `frames_options` to control if a water level is accepted. If the signal to noise is lower than
7
+ set threshold, the next `frames_options` will be used to attempt resolving a water level with a higher signal to
8
+ noise ratio. This allows for treatment of videos under varying conditions, in which water level detection may
9
+ profit from different video treatments.
10
+ ### Deprecated
11
+ ### Removed
12
+ ### Fixed
13
+ - docstring of masks indentation issue
14
+ - CrossSection docstring Sphinx repetition removed.
15
+
16
+
1
17
  ## [0.8.11] = 2025-10-01
2
18
  ### Added
3
19
  - Derivation of transect properties surface area and wetted perimeter.
4
20
  - Added method `transect.get_v_surf` for average surface velocity, and `transect.get_v_bulk` for bulk velocity.
5
21
  ### Changed
6
- - Using `add_text=True` in `transect.plot` now also displays average surface and bulk velocity.
22
+ - Using `add_text=True` in `transect.plot` now also displays average surface and bulk velocity.
7
23
  ### Deprecated
8
24
  ### Removed
9
25
  - `transect.get_wetted_perspective` is no longer required as this can be derived from `transect.cross_section`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyopenrivercam
3
- Version: 0.8.11
3
+ Version: 0.8.12
4
4
  Summary: pyorc: free and open-source image-based surface velocity and discharge.
5
5
  Author-email: Hessel Winsemius <winsemius@rainbowsensing.com>
6
6
  Requires-Python: >=3.9
@@ -1,6 +1,6 @@
1
1
  """pyorc: free and open-source image-based surface velocity and discharge."""
2
2
 
3
- __version__ = "0.8.11"
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
@@ -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(self, h: float, perimeter: bool = False) -> Union[geometry.MultiPolygon, geometry.MultiLineString]:
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 optically from provided image.
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
- if min_z is None:
1152
- if min_h is not None:
1153
- min_z = self.camera_config.h_to_z(min_h)
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
- # determine the relevant start point if only one is used
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
@@ -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
- 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``.
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
- """Mass values with too low correlation.
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
- wdw_x_min : int, optional
308
- window size in negative x-direction of grid (must be negative), overrules wdw in negative x-direction
309
- if 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
314
- if set.
315
- wdw_y_max : int, optional
316
- window size in positive y-direction of grid, overrules wdw in positive x-direction if set.
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 neighbours (inc. itself).
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
- wdw_x_min : int, optional
343
- window size in negative x-direction of grid (must be negative), overrules wdw in negative x-direction
344
- if set.
345
- wdw_x_max : int, optional
346
- window size in positive x-direction of grid, overrules wdw in positive x-direction if set.
347
- wdw_y_min : int, optional
348
- window size in negative y-direction of grid (must be negative), overrules wdw in negative y-direction
349
- if set.
350
- wdw_y_max : int, optional
351
- window size in positive y-direction of grid, overrules wdw in positive x-direction if set.
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
@@ -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 += f"$h_a$: {_ds.transect.h_a:1.2f} m | $v_{{surf}}$: {v_surf.values:1.2f} m/s | $\overline{{v}}$: {v_bulk.values:1.2f} m/s\n$Q$: {Q.values:1.2f} m3/s" # .format(_ds.transect.h_a, Q.values)
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)
@@ -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
- polygon_coords = list(pol.exterior.coords)
1042
- mask = np.zeros_like(img, dtype=np.uint8)
1043
- cv2.fillPoly(mask, np.array([polygon_coords], np.int32), color=255)
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
- return np.flipud(img)[mask == 255]
1046
- return img[mask == 255]
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):
@@ -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 method not in ["grayscale", "hue", "sat", "val"]:
41
- raise ValueError(
42
- f"Method {method} not supported for water level detection, choose one"
43
- f" of {const.ALLOWED_COLOR_METHODS_WATER_LEVEL}"
44
- )
45
- da_frames = video.get_frames(method=method)[n_start:n_end]
46
- # preprocess
47
- da_frames = apply_methods(da_frames, "frames", logger=logger, skip_args=["to_video"], **frames_options)
48
- # if preprocessing still results in a time dim, average in time
49
- if "time" in da_frames.dims:
50
- da_mean = da_frames.mean(dim="time")
51
- else:
52
- da_mean = da_frames
53
- # extract the image
54
- img = np.uint8(da_mean.values)
55
- h_a = cross_section.detect_water_level(img, **water_level_options)
56
- return h_a
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
- h_a = get_water_level(self.video_obj, cross_section=self.cross_section_wl, **kwargs)
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