pyopenrivercam 0.8.10__tar.gz → 0.8.11__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.10 → pyopenrivercam-0.8.11}/CHANGELOG.md +12 -0
  2. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/PKG-INFO +1 -1
  3. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/__init__.py +1 -1
  4. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/api/cross_section.py +28 -5
  5. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/api/plot.py +4 -2
  6. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/api/transect.py +65 -73
  7. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/.gitignore +0 -0
  8. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/.pre-commit-config.yaml +0 -0
  9. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/Dockerfile +0 -0
  10. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/LICENSE +0 -0
  11. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/README.md +0 -0
  12. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/TRADEMARK.md +0 -0
  13. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/environment.yml +0 -0
  14. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/api/__init__.py +0 -0
  15. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/api/cameraconfig.py +0 -0
  16. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/api/frames.py +0 -0
  17. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/api/mask.py +0 -0
  18. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/api/orcbase.py +0 -0
  19. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/api/velocimetry.py +0 -0
  20. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/api/video.py +0 -0
  21. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/cli/__init__.py +0 -0
  22. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/cli/cli_elements.py +0 -0
  23. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/cli/cli_utils.py +0 -0
  24. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/cli/log.py +0 -0
  25. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/cli/main.py +0 -0
  26. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/const.py +0 -0
  27. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/cv.py +0 -0
  28. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/helpers.py +0 -0
  29. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/plot_helpers.py +0 -0
  30. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/project.py +0 -0
  31. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/pyorc.sh +0 -0
  32. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/sample_data.py +0 -0
  33. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/service/__init__.py +0 -0
  34. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/service/camera_config.py +0 -0
  35. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/service/velocimetry.py +0 -0
  36. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/velocimetry/__init__.py +0 -0
  37. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/velocimetry/ffpiv.py +0 -0
  38. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyorc/velocimetry/openpiv.py +0 -0
  39. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/pyproject.toml +0 -0
  40. {pyopenrivercam-0.8.10 → pyopenrivercam-0.8.11}/sonar-project.properties +0 -0
@@ -1,3 +1,15 @@
1
+ ## [0.8.11] = 2025-10-01
2
+ ### Added
3
+ - Derivation of transect properties surface area and wetted perimeter.
4
+ - Added method `transect.get_v_surf` for average surface velocity, and `transect.get_v_bulk` for bulk velocity.
5
+ ### Changed
6
+ - Using `add_text=True` in `transect.plot` now also displays average surface and bulk velocity.
7
+ ### Deprecated
8
+ ### Removed
9
+ - `transect.get_wetted_perspective` is no longer required as this can be derived from `transect.cross_section`.
10
+ - `transect.get_xyz_perspective` is no longer required as this can be derived from `transect.cross_section`.
11
+ ### Fixed
12
+
1
13
  ## [0.8.10] = 2025-09-22
2
14
  ### Added
3
15
  - option `ensemble_corr` with `frames.get_piv`. This performs ensemble correlation averaging on cross-correlation and
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyopenrivercam
3
- Version: 0.8.10
3
+ Version: 0.8.11
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.10"
3
+ __version__ = "0.8.11"
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
@@ -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
 
@@ -613,16 +613,21 @@ 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) -> geometry.Polygon:
617
- """Retrieve a wetted surface perpendicular to flow direction (SZ) for a water level, as a geometry.Polygon.
616
+ def get_wetted_surface_sz(self, h: float, perimeter: bool = False) -> Union[geometry.MultiPolygon, geometry.MultiLineString]:
617
+ """Retrieve a wetted surface or perimeter perpendicular to flow direction (SZ) for a water level.
618
618
 
619
- This is a useful method for instance to estimate m2 wetted surface for a given water level in the cross
620
- section.
619
+ This returns a `geometry.MultiPolygon` when a surface is requested (`perimeter=False`), and
620
+ `geometry.MultiLineString` when a perimeter is requested (`perimeter=True`).
621
+
622
+ This is a useful method for instance to estimate m2 wetted surface or m wetted perimeter length for a given
623
+ water level in the cross section.
621
624
 
622
625
  Parameters
623
626
  ----------
624
627
  h : float
625
628
  water level [m]
629
+ perimeter : bool, optional
630
+ If set to True, return a linestring with the wetted perimeter instead.
626
631
 
627
632
  Returns
628
633
  -------
@@ -630,6 +635,12 @@ class CrossSection:
630
635
  Wetted surface as a polygon, in Y-Z projection.
631
636
 
632
637
  """
638
+
639
+ def avg_y(line):
640
+ """Compute average y-coordinate of a line."""
641
+ ys = [p[1] for p in line.coords]
642
+ return sum(ys) / len(ys)
643
+
633
644
  wl = self.get_cs_waterlevel(
634
645
  h=h, sz=True, extend_by=0.1
635
646
  ) # extend a small bit to guarantee crossing with the bottom coordinates
@@ -642,6 +653,18 @@ class CrossSection:
642
653
  if bottom_points[-1].y < zl:
643
654
  bottom_points.append(geometry.Point(bottom_points[-1].x, zl + 0.1))
644
655
  bottom_line = geometry.LineString(bottom_points)
656
+ if perimeter:
657
+ wl_z = wl.coords[0][-1]
658
+ split_segments = split(bottom_line, wl)
659
+ filtered = []
660
+ for seg in split_segments.geoms:
661
+ seg_z = avg_y(seg)
662
+ if seg_z < wl_z:
663
+ # segment is below water level, add to perimeter
664
+ filtered.append(seg)
665
+
666
+ return geometry.MultiLineString(filtered)
667
+ # return wetted_perim
645
668
  pol = list(polygonize(wl.union(bottom_line)))
646
669
  if len(pol) == 0:
647
670
  # create infinitely small polygon at lowest z coordinate
@@ -752,8 +752,10 @@ def plot_text(ax, ds, prefix, suffix):
752
752
  yloc = 0.95
753
753
  _ds.transect.get_river_flow(q_name="q")
754
754
  Q = np.abs(_ds.river_flow)
755
+ v_surf = _ds.transect.get_v_surf()
756
+ v_bulk = _ds.transect.get_v_bulk()
755
757
  string = prefix
756
- string += "Water level: {:1.2f} m\nDischarge: {:1.2f} m3/s".format(_ds.transect.h_a, Q.values)
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)
757
759
  if "q_nofill" in ds:
758
760
  _ds.transect.get_river_flow(q_name="q_nofill")
759
761
  Q_nofill = np.abs(_ds.river_flow)
@@ -765,7 +767,7 @@ def plot_text(ax, ds, prefix, suffix):
765
767
  xloc,
766
768
  yloc,
767
769
  string,
768
- size=24,
770
+ size=18,
769
771
  horizontalalignment="right",
770
772
  verticalalignment="top",
771
773
  path_effects=path_effects,
@@ -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 get_xyz_perspective(self, trans_mat=None, xs=None, ys=None, mask_outside=True):
181
- """Get camera-perspective column, row coordinates from cross-section locations.
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
- trans_mat : np.ndarray, optional
186
- perspective transform matrix (Default value = None)
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
- cols : list of ints
197
- columns of locations in original camera perspective
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
- if xs is None:
204
- xs = self._obj.x.values
205
- if ys is None:
206
- ys = self._obj.y.values
207
- # compute bathymetry as measured in local height reference (such as staff gauge)
208
- # if self.camera_config.gcps["h_ref"] is None:
209
- # h_ref = 0.0
210
- # else:
211
- # h_ref = self.camera_config.gcps["h_ref"]
212
- hs = self.camera_config.z_to_h(self._obj.zcoords).values
213
- # zs = (self._obj.zcoords - self.camera_config.gcps["z_0"] + h_ref).values
214
- if trans_mat is None:
215
- ms = [self.camera_config.get_M(h, reverse=True, to_bbox_grid=True) for h in hs]
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
- # use user defined M instead
218
- ms = [trans_mat for _ in hs]
219
- # compute row and column position of vectors in original reprojected background image col/row coordinates
220
- cols, rows = zip(
221
- *[
222
- helpers.xy_to_perspective(
223
- x, y, self.camera_config.resolution, trans_mat, reverse_y=self.camera_config.shape[0]
224
- )
225
- for x, y, trans_mat in zip(xs, ys, ms)
226
- ],
227
- )
228
- # ensure y coordinates start at the top in the right orientation
229
- shape_y, shape_x = self.camera_shape
230
- rows = shape_y - np.array(rows)
231
- cols = np.array(cols)
232
- if mask_outside:
233
- # remove values that do not fit in the frames
234
- cols[np.any([cols < 0, cols > self.camera_shape[1]], axis=0)] = np.nan
235
- rows[np.any([rows < 0, rows > self.camera_shape[0]], axis=0)] = np.nan
236
-
237
- return cols, rows
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].
File without changes