pyopenrivercam 0.8.11__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyopenrivercam-0.8.11.dist-info → pyopenrivercam-0.9.0.dist-info}/METADATA +27 -10
- pyopenrivercam-0.9.0.dist-info/RECORD +33 -0
- pyorc/__init__.py +1 -1
- pyorc/api/cross_section.py +280 -19
- pyorc/api/frames.py +17 -44
- pyorc/api/mask.py +40 -36
- pyorc/api/plot.py +11 -7
- pyorc/cv.py +35 -6
- pyorc/helpers.py +3 -9
- pyorc/sample_data.py +24 -3
- pyorc/service/velocimetry.py +37 -19
- pyorc/velocimetry/__init__.py +1 -2
- pyorc/velocimetry/ffpiv.py +2 -3
- pyopenrivercam-0.8.11.dist-info/RECORD +0 -34
- pyorc/velocimetry/openpiv.py +0 -331
- {pyopenrivercam-0.8.11.dist-info → pyopenrivercam-0.9.0.dist-info}/WHEEL +0 -0
- {pyopenrivercam-0.8.11.dist-info → pyopenrivercam-0.9.0.dist-info}/entry_points.txt +0 -0
- {pyopenrivercam-0.8.11.dist-info → pyopenrivercam-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyopenrivercam
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
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
|
|
@@ -21,23 +21,22 @@ Requires-Dist: click
|
|
|
21
21
|
Requires-Dist: cython; platform_machine == 'armv7l'
|
|
22
22
|
Requires-Dist: dask
|
|
23
23
|
Requires-Dist: descartes
|
|
24
|
-
Requires-Dist: ffpiv>=0.
|
|
24
|
+
Requires-Dist: ffpiv>=0.2.0
|
|
25
25
|
Requires-Dist: flox
|
|
26
26
|
Requires-Dist: geojson
|
|
27
27
|
Requires-Dist: geopandas
|
|
28
28
|
Requires-Dist: matplotlib
|
|
29
29
|
Requires-Dist: netCDF4
|
|
30
|
-
Requires-Dist: numba
|
|
31
|
-
Requires-Dist: numpy
|
|
30
|
+
Requires-Dist: numba>0.56
|
|
31
|
+
Requires-Dist: numpy>2
|
|
32
32
|
Requires-Dist: opencv-python
|
|
33
|
-
Requires-Dist: openpiv
|
|
34
33
|
Requires-Dist: packaging; platform_machine == 'armv7l'
|
|
35
34
|
Requires-Dist: pip
|
|
36
35
|
Requires-Dist: pyproj
|
|
37
36
|
Requires-Dist: pythran; platform_machine == 'armv7l'
|
|
38
37
|
Requires-Dist: pyyaml
|
|
39
|
-
Requires-Dist: rasterio<1.4.0
|
|
40
|
-
Requires-Dist:
|
|
38
|
+
Requires-Dist: rasterio<1.4.0; python_version <= '3.12'
|
|
39
|
+
Requires-Dist: rasterio; python_version > '3.12'
|
|
41
40
|
Requires-Dist: scipy
|
|
42
41
|
Requires-Dist: shapely
|
|
43
42
|
Requires-Dist: tqdm
|
|
@@ -112,11 +111,15 @@ We are seeking funding for the following frequently requested functionalities:
|
|
|
112
111
|
|
|
113
112
|
If you wish to fund this or other work on features, please contact us at info@rainbowsensing.com.
|
|
114
113
|
|
|
115
|
-
>
|
|
114
|
+
> [!NOTE]
|
|
115
|
+
> For instructions how to get Anaconda (with lots of pre-installed libraries) or Miniconda (light weight) installed,
|
|
116
|
+
> please go to https://docs.conda.io/projects/conda/en/latest/
|
|
116
117
|
|
|
117
|
-
>
|
|
118
|
+
> [!TIP]
|
|
119
|
+
> Please go to https://localdevices.github.io/pyorc for the latest documentation
|
|
118
120
|
|
|
119
|
-
>
|
|
121
|
+
> [!IMPORTANT]
|
|
122
|
+
> At this moment **pyorc** works with any video compatible with OpenCV as long as it has proper metadata.
|
|
120
123
|
|
|
121
124
|
## Installation
|
|
122
125
|
You need a python environment. We recommend using the Miniforge project. Download
|
|
@@ -158,6 +161,18 @@ pip install pyopenrivercam[extra]
|
|
|
158
161
|
The `[extra]` section ensures that also geographical plotting is supported, which we recommend especially for the
|
|
159
162
|
set up of a camera configuration with RTK-GPS measured control points.
|
|
160
163
|
|
|
164
|
+
> [!NOTE]
|
|
165
|
+
>
|
|
166
|
+
> Most of the heavy lifting is done while deriving cross-correlations for estimation of velocity vectors with Particle
|
|
167
|
+
> Image Velocimetry. You can speed up this process (x2) by installing `rocket-fft`. With `python <= 3.12` this
|
|
168
|
+
> is automatically included. With higher versions, you need, for the moment, to install it separately as follows:
|
|
169
|
+
>
|
|
170
|
+
> ```shell
|
|
171
|
+
> pip install git+https://github.com/localdevices/rocket-fft.git
|
|
172
|
+
> ```
|
|
173
|
+
>
|
|
174
|
+
> Once rocket-fft gets updated in PyPi you will no longer need this separate installation procedure.
|
|
175
|
+
|
|
161
176
|
### Upgrading from pypi with pip
|
|
162
177
|
|
|
163
178
|
Did you read about a new version and you want to upgrade? Simply activate your virtual environment, type
|
|
@@ -175,6 +190,8 @@ If you use `mamba` as a package manager, then the steps are the same, except for
|
|
|
175
190
|
```shell
|
|
176
191
|
mamba install pyopenrivercam
|
|
177
192
|
```
|
|
193
|
+
The version installed may not have the latest underlying libraries and therefore may be slower than the latest PyPi
|
|
194
|
+
version. We therefore recommend using `pip` for installation (see above).
|
|
178
195
|
|
|
179
196
|
### Installation from latest code base
|
|
180
197
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
pyorc/__init__.py,sha256=yPY8lllCcJb3rKv4-9O6b393ZSX86dAiOSRqAbEBmaQ,523
|
|
2
|
+
pyorc/const.py,sha256=Ia0KRkm-E1lJk4NxQVPDIfN38EBB7BKvxmwIHJrGPUY,2597
|
|
3
|
+
pyorc/cv.py,sha256=t2ZR4eyGbiwlIaGHysOheWdaDQuqpWLKjcTiAUzWAR0,50261
|
|
4
|
+
pyorc/helpers.py,sha256=jed0YyywnpvsZS-8mcA7Lfzn9np1MTlmVLE_PDn2QY0,30454
|
|
5
|
+
pyorc/plot_helpers.py,sha256=gLKslspsF_Z4jib5jkBv2wRjKnHTbuRFgkp_PCmv-uU,1803
|
|
6
|
+
pyorc/project.py,sha256=CGKfICkQEpFRmh_ZeDEfbQ-wefJt7teWJd6B5IPF038,7747
|
|
7
|
+
pyorc/pyorc.sh,sha256=-xOSUNnMAwVbdNkjKNKMZMaBljWsGLhadG-j0DNlJP4,5
|
|
8
|
+
pyorc/sample_data.py,sha256=_yxtjhHc1sjHXZJRWQgBNOOn0Qqs2A5CavyFOQX5p8U,3241
|
|
9
|
+
pyorc/api/__init__.py,sha256=k2OQQH4NrtXTuVm23d0g_SX6H5DhnKC9_kDyzJ4dWdk,428
|
|
10
|
+
pyorc/api/cameraconfig.py,sha256=NP9F7LhPO3aO6FRWkrGl6XpX8O3K59zfTtaYR3Kujqw,65419
|
|
11
|
+
pyorc/api/cross_section.py,sha256=MH0AEw5K1Kc1ClZeQRBUYkShZYVk41fshLn6GzCZAas,65212
|
|
12
|
+
pyorc/api/frames.py,sha256=BnglhmHdbKlIip5tym3x-aICOpQRmm853109A7JkWk8,23189
|
|
13
|
+
pyorc/api/mask.py,sha256=A3TRMqi30L4N491C4FoYY0zvV1GwQ1U31OEkCJp_Nzc,16698
|
|
14
|
+
pyorc/api/orcbase.py,sha256=C23QTKOyxHUafyJsq_t7xn_BzAEvf4DDfzlYAopons8,4189
|
|
15
|
+
pyorc/api/plot.py,sha256=MxIEIS8l46bUaca0GtMazx8-k2_TbfQLrPCPAjuWos8,31082
|
|
16
|
+
pyorc/api/transect.py,sha256=wENKWt0u0lHtT0lPYv47faHf_iAN9Mmeev-vwWjnz6E,13382
|
|
17
|
+
pyorc/api/velocimetry.py,sha256=bfU_XPbUbrdBI2XGprzh_3YADbGHfy4OuS1oBlbLEEI,12047
|
|
18
|
+
pyorc/api/video.py,sha256=lGD6bcV6Uu2u3zuGF_m3KxX2Cyp9k-YHUiXA42TOE3E,22458
|
|
19
|
+
pyorc/cli/__init__.py,sha256=A7hOQV26vIccPnDc8L2KqoJOSpMpf2PiMOXS18pAsWg,32
|
|
20
|
+
pyorc/cli/cli_elements.py,sha256=zX9wv9-1KWC_E3cInGMm3g9jh4uXmT2NqooAMhhXR9s,22165
|
|
21
|
+
pyorc/cli/cli_utils.py,sha256=S7qOO4bintxXDSUl26u3Ujqu4JHb_TNhw5d6psyDrFo,15085
|
|
22
|
+
pyorc/cli/log.py,sha256=Vg8GznmrEPqijfW6wv4OCl8R00Ld_fVt-ULTitaDijY,2824
|
|
23
|
+
pyorc/cli/main.py,sha256=qhAZkUuAViCpHh9c19tpcpbs_xoZJkYHhOsEXJBFXfM,12742
|
|
24
|
+
pyorc/service/__init__.py,sha256=vPrzFlZ4e_GjnibwW6-k8KDz3b7WpgmGcwSDk0mr13Y,55
|
|
25
|
+
pyorc/service/camera_config.py,sha256=OsRLpe5jd-lu6HT4Vx5wEg554CMS-IKz-q62ir4VbPo,2375
|
|
26
|
+
pyorc/service/velocimetry.py,sha256=bPI1OdN_fi0gZES08mb7yqCS_4I-lKSZ2JvWSGTRD1E,34434
|
|
27
|
+
pyorc/velocimetry/__init__.py,sha256=5oShoMocCalcCZuIsBqlZlqQuKJgDDBUvXQIo-uqFPA,88
|
|
28
|
+
pyorc/velocimetry/ffpiv.py,sha256=92XDgzCW4mEZ5ow82zV0APOhfDc1OVftBjKqYdw1zzc,17494
|
|
29
|
+
pyopenrivercam-0.9.0.dist-info/entry_points.txt,sha256=Cv_WI2Y6QLnPiNCXGli0gS4WAOAeMoprha1rAR3vdRE,44
|
|
30
|
+
pyopenrivercam-0.9.0.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
|
31
|
+
pyopenrivercam-0.9.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
32
|
+
pyopenrivercam-0.9.0.dist-info/METADATA,sha256=d2rvFtUmIHAhMBruxLKJ8qEukxEl3y4fZWzAbeeNZ0w,12383
|
|
33
|
+
pyopenrivercam-0.9.0.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.
|
|
3
|
+
__version__ = "0.9.0"
|
|
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/frames.py
CHANGED
|
@@ -12,7 +12,7 @@ from matplotlib.animation import FuncAnimation
|
|
|
12
12
|
from tqdm import tqdm
|
|
13
13
|
|
|
14
14
|
from pyorc import const, cv, helpers, project
|
|
15
|
-
from pyorc.velocimetry import ffpiv
|
|
15
|
+
from pyorc.velocimetry import ffpiv
|
|
16
16
|
|
|
17
17
|
from .orcbase import ORCBase
|
|
18
18
|
from .plot import _frames_plot
|
|
@@ -124,14 +124,12 @@ class Frames(ORCBase):
|
|
|
124
124
|
overlap : (int, int), optional
|
|
125
125
|
amount of overlap between interrogation windows in pixels (y, x)
|
|
126
126
|
engine : str, optional
|
|
127
|
-
select the compute engine, can be "
|
|
128
|
-
performance
|
|
129
|
-
to
|
|
127
|
+
select the compute engine, can be "numba" (default), or "numpy". "numba" will give the fastest
|
|
128
|
+
performance. It can boost performance by almost an order of magnitude compared
|
|
129
|
+
to numpy. both "numba" and "numpy" use the FF-PIV library as back-end.
|
|
130
130
|
ensemble_corr : bool, optional
|
|
131
|
-
only used with `engine="numba"` or `engine="numpy"`.
|
|
132
131
|
If True, performs PIV by first averaging cross-correlations across all frames and then deriving velocities.
|
|
133
132
|
If False, computes velocities for each frame pair separately. Default is True.
|
|
134
|
-
|
|
135
133
|
**kwargs : dict
|
|
136
134
|
keyword arguments to pass to the piv engine. For "numba" and "numpy" the argument `chunks` can be provided
|
|
137
135
|
with an integer defining in how many batches of work the total velocimetry problem should be subdivided.
|
|
@@ -143,7 +141,6 @@ class Frames(ORCBase):
|
|
|
143
141
|
|
|
144
142
|
See Also
|
|
145
143
|
--------
|
|
146
|
-
OpenPIV project: https://github.com/OpenPIV/openpiv-python
|
|
147
144
|
FF-PIV project: https://github.com/localdevices/ffpiv
|
|
148
145
|
|
|
149
146
|
"""
|
|
@@ -167,43 +164,19 @@ class Frames(ORCBase):
|
|
|
167
164
|
# get all required coordinates for the PIV result
|
|
168
165
|
coords, mesh_coords = self.get_piv_coords(window_size, search_area_size, overlap)
|
|
169
166
|
# provide kwargs for OpenPIV analysis
|
|
170
|
-
if engine
|
|
171
|
-
# thresholds are not used.
|
|
172
|
-
|
|
173
|
-
import warnings
|
|
174
|
-
|
|
175
|
-
warnings.warn(
|
|
176
|
-
'"openpiv" is deprecated, please use "numba" or "numpy" as engine',
|
|
177
|
-
DeprecationWarning,
|
|
178
|
-
stacklevel=2,
|
|
179
|
-
)
|
|
180
|
-
# Remove threshold parameters from kwargs
|
|
181
|
-
kwargs.pop("corr_min", None)
|
|
182
|
-
kwargs.pop("s2n_min", None)
|
|
183
|
-
kwargs.pop("count_min", None)
|
|
184
|
-
kwargs = {
|
|
185
|
-
**kwargs,
|
|
186
|
-
"search_area_size": search_area_size[0],
|
|
187
|
-
"window_size": window_size[0],
|
|
188
|
-
"overlap": overlap[0],
|
|
189
|
-
"res_x": camera_config.resolution,
|
|
190
|
-
"res_y": camera_config.resolution,
|
|
191
|
-
}
|
|
192
|
-
ds = openpiv.get_openpiv(self._obj, coords["y"], coords["x"], dt, **kwargs)
|
|
193
|
-
elif engine in ["numba", "numpy"]:
|
|
194
|
-
kwargs = {
|
|
195
|
-
**kwargs,
|
|
196
|
-
"search_area_size": search_area_size,
|
|
197
|
-
"window_size": window_size,
|
|
198
|
-
"overlap": overlap,
|
|
199
|
-
"res_x": camera_config.resolution,
|
|
200
|
-
"res_y": camera_config.resolution,
|
|
201
|
-
}
|
|
202
|
-
ds = ffpiv.get_ffpiv(
|
|
203
|
-
self._obj, coords["y"], coords["x"], dt, engine=engine, ensemble_corr=ensemble_corr, **kwargs
|
|
204
|
-
)
|
|
205
|
-
else:
|
|
167
|
+
if engine not in ["numba", "numpy"]:
|
|
206
168
|
raise ValueError(f"Selected PIV engine {engine} does not exist.")
|
|
169
|
+
kwargs = {
|
|
170
|
+
**kwargs,
|
|
171
|
+
"search_area_size": search_area_size,
|
|
172
|
+
"window_size": window_size,
|
|
173
|
+
"overlap": overlap,
|
|
174
|
+
"res_x": camera_config.resolution,
|
|
175
|
+
"res_y": camera_config.resolution,
|
|
176
|
+
}
|
|
177
|
+
ds = ffpiv.get_ffpiv(
|
|
178
|
+
self._obj, coords["y"], coords["x"], dt, engine=engine, ensemble_corr=ensemble_corr, **kwargs
|
|
179
|
+
)
|
|
207
180
|
# add all 2D-coordinates
|
|
208
181
|
ds = ds.velocimetry.add_xy_coords(mesh_coords, coords, {**const.PERSPECTIVE_ATTRS, **const.GEOGRAPHICAL_ATTRS})
|
|
209
182
|
# ensure all metadata is transferred
|
|
@@ -359,7 +332,7 @@ class Frames(ORCBase):
|
|
|
359
332
|
keep_attrs=True,
|
|
360
333
|
)
|
|
361
334
|
|
|
362
|
-
def minmax(self, min=-np.
|
|
335
|
+
def minmax(self, min=-np.inf, max=np.inf):
|
|
363
336
|
"""Minimum / maximum intensity filter.
|
|
364
337
|
|
|
365
338
|
All pixels will be thresholded to a minimum and maximum value.
|