pyopenrivercam 0.8.6__tar.gz → 0.8.8__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.
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/CHANGELOG.md +35 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/PKG-INFO +4 -4
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/README.md +3 -3
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/__init__.py +1 -1
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/api/cameraconfig.py +140 -37
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/api/cross_section.py +139 -34
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/api/frames.py +19 -58
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/api/plot.py +94 -36
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/cli/cli_utils.py +7 -9
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/cli/main.py +12 -3
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/cv.py +111 -24
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/helpers.py +23 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/plot_helpers.py +21 -6
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/service/velocimetry.py +238 -176
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyproject.toml +7 -0
- pyopenrivercam-0.8.8/sonar-project.properties +18 -0
- pyopenrivercam-0.8.6/.github/ISSUE_TEMPLATE/issue-report.md +0 -31
- pyopenrivercam-0.8.6/.github/workflows/documentation.yml +0 -60
- pyopenrivercam-0.8.6/.github/workflows/pypi-publish.yml +0 -57
- pyopenrivercam-0.8.6/.github/workflows/pypitest-publish.yml +0 -60
- pyopenrivercam-0.8.6/.github/workflows/tests.yml +0 -61
- pyopenrivercam-0.8.6/docs/Makefile +0 -21
- pyopenrivercam-0.8.6/docs/_images/ChuoKikuu_GCPs.jpg +0 -0
- pyopenrivercam-0.8.6/docs/_images/GCPs_interactive.jpg +0 -0
- pyopenrivercam-0.8.6/docs/_images/Geul_GCPs.jpg +0 -0
- pyopenrivercam-0.8.6/docs/_images/bbox_interactive.jpg +0 -0
- pyopenrivercam-0.8.6/docs/_images/camera_calib.gif +0 -0
- pyopenrivercam-0.8.6/docs/_images/example_geul_small.jpg +0 -0
- pyopenrivercam-0.8.6/docs/_images/site_schematic_cs.svg +0 -1
- pyopenrivercam-0.8.6/docs/_images/site_schematic_planar.svg +0 -1
- pyopenrivercam-0.8.6/docs/_images/video_edge.gif +0 -0
- pyopenrivercam-0.8.6/docs/_images/video_norm.gif +0 -0
- pyopenrivercam-0.8.6/docs/_images/video_norm_proj.gif +0 -0
- pyopenrivercam-0.8.6/docs/_images/video_orig.gif +0 -0
- pyopenrivercam-0.8.6/docs/_images/video_stable.gif +0 -0
- pyopenrivercam-0.8.6/docs/_images/video_unstable.gif +0 -0
- pyopenrivercam-0.8.6/docs/_images/wark_cam_config.jpg +0 -0
- pyopenrivercam-0.8.6/docs/_images/wark_cam_config_persp.jpg +0 -0
- pyopenrivercam-0.8.6/docs/_images/wark_cam_persp.jpg +0 -0
- pyopenrivercam-0.8.6/docs/_images/wark_discharge.jpg +0 -0
- pyopenrivercam-0.8.6/docs/_images/wark_streamplot.jpg +0 -0
- pyopenrivercam-0.8.6/docs/_static/logo.png +0 -0
- pyopenrivercam-0.8.6/docs/_static/logo.svg +0 -1544
- pyopenrivercam-0.8.6/docs/_static/orc_favicon.svg +0 -220
- pyopenrivercam-0.8.6/docs/_static/orc_logo_bw.svg +0 -179
- pyopenrivercam-0.8.6/docs/_static/orc_logo_color.svg +0 -182
- pyopenrivercam-0.8.6/docs/_static/orc_logo_grey.svg +0 -179
- pyopenrivercam-0.8.6/docs/_static/theme-localdevices.css +0 -31
- pyopenrivercam-0.8.6/docs/_templates/accessor_method.rst +0 -6
- pyopenrivercam-0.8.6/docs/api.rst +0 -412
- pyopenrivercam-0.8.6/docs/conf.py +0 -136
- pyopenrivercam-0.8.6/docs/index.rst +0 -87
- pyopenrivercam-0.8.6/docs/installation.rst +0 -114
- pyopenrivercam-0.8.6/docs/intro.rst +0 -47
- pyopenrivercam-0.8.6/docs/make.bat +0 -35
- pyopenrivercam-0.8.6/docs/ngwerere.jpg +0 -0
- pyopenrivercam-0.8.6/docs/quickstart.rst +0 -45
- pyopenrivercam-0.8.6/docs/requirements.txt +0 -12
- pyopenrivercam-0.8.6/docs/user-guide/api.rst +0 -28
- pyopenrivercam-0.8.6/docs/user-guide/camera_config/api.rst +0 -39
- pyopenrivercam-0.8.6/docs/user-guide/camera_config/api_bbox.rst +0 -50
- pyopenrivercam-0.8.6/docs/user-guide/camera_config/api_gcps.rst +0 -78
- pyopenrivercam-0.8.6/docs/user-guide/camera_config/api_geo.rst +0 -50
- pyopenrivercam-0.8.6/docs/user-guide/camera_config/api_lens_calib.rst +0 -21
- pyopenrivercam-0.8.6/docs/user-guide/camera_config/cam_config.py +0 -41
- pyopenrivercam-0.8.6/docs/user-guide/camera_config/cli.rst +0 -30
- pyopenrivercam-0.8.6/docs/user-guide/camera_config/cli_bbox.rst +0 -27
- pyopenrivercam-0.8.6/docs/user-guide/camera_config/cli_gcps.rst +0 -79
- pyopenrivercam-0.8.6/docs/user-guide/camera_config/cli_geo.rst +0 -18
- pyopenrivercam-0.8.6/docs/user-guide/camera_config/index.rst +0 -471
- pyopenrivercam-0.8.6/docs/user-guide/cli.rst +0 -126
- pyopenrivercam-0.8.6/docs/user-guide/cross_section/cross_section.jpg +0 -0
- pyopenrivercam-0.8.6/docs/user-guide/cross_section/index.rst +0 -323
- pyopenrivercam-0.8.6/docs/user-guide/cross_section/polygon_samples.jpg +0 -0
- pyopenrivercam-0.8.6/docs/user-guide/cross_section/pyplots/cross.py +0 -21
- pyopenrivercam-0.8.6/docs/user-guide/cross_section/wrong_estimate.jpg +0 -0
- pyopenrivercam-0.8.6/docs/user-guide/frames/frames.ipynb +0 -296
- pyopenrivercam-0.8.6/docs/user-guide/frames/index.rst +0 -211
- pyopenrivercam-0.8.6/docs/user-guide/index.rst +0 -98
- pyopenrivercam-0.8.6/docs/user-guide/plot/index.rst +0 -182
- pyopenrivercam-0.8.6/docs/user-guide/plot/plot_example.jpg +0 -0
- pyopenrivercam-0.8.6/docs/user-guide/plot/plot_example.py +0 -56
- pyopenrivercam-0.8.6/docs/user-guide/plot/plot_example.yml +0 -26
- pyopenrivercam-0.8.6/docs/user-guide/transect/index.rst +0 -113
- pyopenrivercam-0.8.6/docs/user-guide/velocimetry/index.rst +0 -388
- pyopenrivercam-0.8.6/docs/user-guide/video/index.rst +0 -307
- pyopenrivercam-0.8.6/envs/pyorc-dev.yml +0 -37
- pyopenrivercam-0.8.6/envs/pyorc-test.yml +0 -40
- pyopenrivercam-0.8.6/examples/01_Camera_Configuration_single_video.ipynb +0 -289
- pyopenrivercam-0.8.6/examples/02_Process_velocimetry.ipynb +0 -289
- pyopenrivercam-0.8.6/examples/03_Plotting_and_masking_velocimetry_results.ipynb +0 -341
- pyopenrivercam-0.8.6/examples/04_Extracting_crosssection_velocities_and_discharge.ipynb +0 -321
- pyopenrivercam-0.8.6/examples/05_Camera_calibration.ipynb +0 -247
- pyopenrivercam-0.8.6/examples/06_Estimating_water_levels_optically.ipynb +0 -299
- pyopenrivercam-0.8.6/examples/camera_calib/camera_calib_720p.mkv +0 -0
- pyopenrivercam-0.8.6/examples/geul/dk_cam_config.json +0 -104
- pyopenrivercam-0.8.6/examples/geul/dk_control.mp4 +0 -0
- pyopenrivercam-0.8.6/examples/ngwerere/cross_section1.geojson +0 -33
- pyopenrivercam-0.8.6/examples/ngwerere/cross_section2.geojson +0 -31
- pyopenrivercam-0.8.6/examples/ngwerere/ngwerere.json +0 -84
- pyopenrivercam-0.8.6/examples/ngwerere/ngwerere.yml +0 -91
- pyopenrivercam-0.8.6/examples/ngwerere/ngwerere_20191103.mp4 +0 -0
- pyopenrivercam-0.8.6/examples/ngwerere/ngwerere_cross_section.csv +0 -28
- pyopenrivercam-0.8.6/examples/ngwerere/ngwerere_cross_section_2.csv +0 -26
- pyopenrivercam-0.8.6/examples/ngwerere/ngwerere_gcps.geojson +0 -11
- pyopenrivercam-0.8.6/examples/ngwerere/ngwerere_masked.nc +0 -0
- pyopenrivercam-0.8.6/examples/ngwerere/ngwerere_piv.nc +0 -0
- pyopenrivercam-0.8.6/examples/ngwerere/ngwerere_test.yml +0 -68
- pyopenrivercam-0.8.6/tests/__init__.py +0 -0
- pyopenrivercam-0.8.6/tests/conftest.py +0 -444
- pyopenrivercam-0.8.6/tests/test_cameraconfig.py +0 -272
- pyopenrivercam-0.8.6/tests/test_cli.py +0 -240
- pyopenrivercam-0.8.6/tests/test_cross_section.py +0 -395
- pyopenrivercam-0.8.6/tests/test_frames.py +0 -155
- pyopenrivercam-0.8.6/tests/test_mask.py +0 -59
- pyopenrivercam-0.8.6/tests/test_plot_helpers.py +0 -73
- pyopenrivercam-0.8.6/tests/test_sample_data.py +0 -9
- pyopenrivercam-0.8.6/tests/test_transect.py +0 -68
- pyopenrivercam-0.8.6/tests/test_velocimetry.py +0 -42
- pyopenrivercam-0.8.6/tests/test_video.py +0 -70
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/.gitignore +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/.pre-commit-config.yaml +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/Dockerfile +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/LICENSE +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/TRADEMARK.md +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/environment.yml +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/api/__init__.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/api/mask.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/api/orcbase.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/api/transect.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/api/velocimetry.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/api/video.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/cli/__init__.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/cli/cli_elements.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/cli/log.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/const.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/project.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/pyorc.sh +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/sample_data.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/service/__init__.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/service/camera_config.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/velocimetry/__init__.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/velocimetry/ffpiv.py +0 -0
- {pyopenrivercam-0.8.6 → pyopenrivercam-0.8.8}/pyorc/velocimetry/openpiv.py +0 -0
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
## [0.8.8] = 2025-07-14
|
|
2
|
+
### Added
|
|
3
|
+
### Changed
|
|
4
|
+
### Deprecated
|
|
5
|
+
### Removed
|
|
6
|
+
### Fixed
|
|
7
|
+
- issues with plotting cross sections in edge cases with water levels equal to lowest point or above highest point
|
|
8
|
+
- fixed problem with situations where either the cross section data from a shapefile or the camera configuration
|
|
9
|
+
did not contain CRS information. This is now correctly parsed when creating a `CrossSection` instance.
|
|
10
|
+
|
|
11
|
+
## [0.8.7] = 2025-06-30
|
|
12
|
+
### Added
|
|
13
|
+
- CLI option `--cross_wl`
|
|
14
|
+
- A new 3-point bounding box estimation, ideal for selecting a bounding box in strongly oblique cases. First select left
|
|
15
|
+
bank, then right bank, then a point up- or downstream from the selected line for the size of the bounding box.
|
|
16
|
+
- `rvec` and `tvec` are written to camera configuration after fitting.
|
|
17
|
+
### Changed
|
|
18
|
+
- CLI option `--cross_wl` now replaces `--cross` for optical water level estimation. `--cross` is only used for
|
|
19
|
+
discharge calculation
|
|
20
|
+
- Plotting in camera objective is accelerated
|
|
21
|
+
- water level detection with `CrossSection.detect_water_level` can now consume either `min_z` and `max_z` (for levels
|
|
22
|
+
in the original coordinate system) or `min_h` and `max_h` (for levels using the local `h_ref` datum if provided).
|
|
23
|
+
These minimum and maximum levels can be used to pre-condition the level range that the algoithm will seek in.
|
|
24
|
+
- Pose fitting can now be performed with a pre-defined camera matrix and set of distortion coefficients. This is very
|
|
25
|
+
useful when a user has already pre-calibrated these parameters. It will improve the fit of the `rvec` and `tvec`,
|
|
26
|
+
i.e. rotation and translation vectors.
|
|
27
|
+
- Changed code coverage reporting from Codecov to sonarqube for code coverage reports. Codecov caused issues with
|
|
28
|
+
numba code.
|
|
29
|
+
### Deprecated
|
|
30
|
+
### Removed
|
|
31
|
+
### Fixed
|
|
32
|
+
- incorrect estimation of optical water level if `bank="near"` was used. This resulted in only a smaller portion of the
|
|
33
|
+
cross section being used. Now the full nearby side is used.
|
|
34
|
+
|
|
1
35
|
## [0.8.6] = 2025-05-16
|
|
2
36
|
### Added
|
|
3
37
|
- added options `--k1`, `--k2` and `--focal_length` to command line interface for cases where
|
|
@@ -5,6 +39,7 @@
|
|
|
5
39
|
### Changed
|
|
6
40
|
- optimization of intrinsics can now also be done with partly already known data. If k1 and k2 are known
|
|
7
41
|
these can be passed as camera
|
|
42
|
+
- Debug messaging increased in `service.velocimetry`.
|
|
8
43
|
### Deprecated
|
|
9
44
|
### Removed
|
|
10
45
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyopenrivercam
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.8
|
|
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
|
|
@@ -78,7 +78,8 @@ width=100 align="right">
|
|
|
78
78
|
|
|
79
79
|
[](https://pypi.org/project/pyopenrivercam)
|
|
80
80
|
[](https://anaconda.org/conda-forge/pyopenrivercam)
|
|
81
|
-
[](https://sonarcloud.io/summary/new_code?id=localdevices_pyorc)
|
|
82
|
+
[](https://pypi.org/project/pyopenrivercam/)
|
|
82
83
|
[](https://localdevices.github.io/pyorc/latest)
|
|
83
84
|
[](https://mybinder.org/v2/gh/localdevices/pyorc.git/main?labpath=examples)
|
|
84
85
|
[](https://github.com/localdevices/pyorc/blob/main/LICENSE)
|
|
@@ -107,7 +108,6 @@ We are seeking funding for the following frequently requested functionalities:
|
|
|
107
108
|
* Exports to simple text formats and GIS-compatible layers
|
|
108
109
|
* Exports to augmented reality videos
|
|
109
110
|
* Implementation of additional processing algorithms (STIV and LSPTV)
|
|
110
|
-
* Implementation of several optical methods for reading water levels
|
|
111
111
|
* Improved nighttime / poor weather conditions processing through learning approaches
|
|
112
112
|
|
|
113
113
|
If you wish to fund this or other work on features, please contact us at info@rainbowsensing.com.
|
|
@@ -156,7 +156,7 @@ dependencies as follows:
|
|
|
156
156
|
pip install pyopenrivercam[extra]
|
|
157
157
|
```
|
|
158
158
|
The `[extra]` section ensures that also geographical plotting is supported, which we recommend especially for the
|
|
159
|
-
set up of a camera configuration.
|
|
159
|
+
set up of a camera configuration with RTK-GPS measured control points.
|
|
160
160
|
|
|
161
161
|
### Upgrading from pypi with pip
|
|
162
162
|
|
|
@@ -8,7 +8,8 @@ width=100 align="right">
|
|
|
8
8
|
|
|
9
9
|
[](https://pypi.org/project/pyopenrivercam)
|
|
10
10
|
[](https://anaconda.org/conda-forge/pyopenrivercam)
|
|
11
|
-
[](https://sonarcloud.io/summary/new_code?id=localdevices_pyorc)
|
|
12
|
+
[](https://pypi.org/project/pyopenrivercam/)
|
|
12
13
|
[](https://localdevices.github.io/pyorc/latest)
|
|
13
14
|
[](https://mybinder.org/v2/gh/localdevices/pyorc.git/main?labpath=examples)
|
|
14
15
|
[](https://github.com/localdevices/pyorc/blob/main/LICENSE)
|
|
@@ -37,7 +38,6 @@ We are seeking funding for the following frequently requested functionalities:
|
|
|
37
38
|
* Exports to simple text formats and GIS-compatible layers
|
|
38
39
|
* Exports to augmented reality videos
|
|
39
40
|
* Implementation of additional processing algorithms (STIV and LSPTV)
|
|
40
|
-
* Implementation of several optical methods for reading water levels
|
|
41
41
|
* Improved nighttime / poor weather conditions processing through learning approaches
|
|
42
42
|
|
|
43
43
|
If you wish to fund this or other work on features, please contact us at info@rainbowsensing.com.
|
|
@@ -86,7 +86,7 @@ dependencies as follows:
|
|
|
86
86
|
pip install pyopenrivercam[extra]
|
|
87
87
|
```
|
|
88
88
|
The `[extra]` section ensures that also geographical plotting is supported, which we recommend especially for the
|
|
89
|
-
set up of a camera configuration.
|
|
89
|
+
set up of a camera configuration with RTK-GPS measured control points.
|
|
90
90
|
|
|
91
91
|
### Upgrading from pypi with pip
|
|
92
92
|
|
|
@@ -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.8"
|
|
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
|
|
@@ -115,6 +115,8 @@ class CameraConfig:
|
|
|
115
115
|
self.height = height
|
|
116
116
|
self.width = width
|
|
117
117
|
self.is_nadir = is_nadir
|
|
118
|
+
self.camera_matrix = camera_matrix
|
|
119
|
+
self.dist_coeffs = dist_coeffs
|
|
118
120
|
self.rvec = rvec
|
|
119
121
|
self.tvec = tvec
|
|
120
122
|
if crs is not None:
|
|
@@ -132,19 +134,14 @@ class CameraConfig:
|
|
|
132
134
|
self.lens_position = None
|
|
133
135
|
if gcps is not None:
|
|
134
136
|
self.set_gcps(**gcps)
|
|
135
|
-
if
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
# camera pars are incomplete and need to be derived
|
|
142
|
-
else:
|
|
143
|
-
self.set_intrinsic(camera_matrix=camera_matrix)
|
|
137
|
+
if self.is_nadir:
|
|
138
|
+
# with nadir, no perspective can be constructed, hence, camera matrix and dist coeffs will be set
|
|
139
|
+
# to default values
|
|
140
|
+
self.camera_matrix = cv.get_cam_mtx(self.height, self.width)
|
|
141
|
+
self.dist_coeffs = cv.DIST_COEFFS
|
|
142
|
+
# camera pars are incomplete and need to be derived
|
|
144
143
|
else:
|
|
145
|
-
|
|
146
|
-
self.camera_matrix = camera_matrix
|
|
147
|
-
self.dist_coeffs = dist_coeffs
|
|
144
|
+
self.calibrate()
|
|
148
145
|
if calibration_video is not None:
|
|
149
146
|
self.set_lens_calibration(calibration_video, plot=False)
|
|
150
147
|
if bbox is not None:
|
|
@@ -324,12 +321,12 @@ class CameraConfig:
|
|
|
324
321
|
tvec_cam += self.gcps_mean
|
|
325
322
|
# transform back to world
|
|
326
323
|
rvec, tvec = cv.pose_world_to_camera(rvec_cam, tvec_cam)
|
|
327
|
-
return
|
|
324
|
+
return rvec, tvec
|
|
328
325
|
|
|
329
326
|
@property
|
|
330
327
|
def rvec(self):
|
|
331
328
|
"""Return rvec from precise N point solution."""
|
|
332
|
-
return self.pnp[
|
|
329
|
+
return self.pnp[0].tolist() if self._rvec is None else self._rvec
|
|
333
330
|
|
|
334
331
|
@rvec.setter
|
|
335
332
|
def rvec(self, _rvec):
|
|
@@ -399,11 +396,11 @@ class CameraConfig:
|
|
|
399
396
|
@property
|
|
400
397
|
def tvec(self):
|
|
401
398
|
"""Return tvec from precise N point solution."""
|
|
402
|
-
return self.pnp[
|
|
399
|
+
return self.pnp[1].tolist() if self._tvec is None else self._tvec
|
|
403
400
|
|
|
404
401
|
@tvec.setter
|
|
405
402
|
def tvec(self, _tvec):
|
|
406
|
-
self._tvec = _tvec.tolist if isinstance(_tvec, np.ndarray) else _tvec
|
|
403
|
+
self._tvec = _tvec.tolist() if isinstance(_tvec, np.ndarray) else _tvec
|
|
407
404
|
|
|
408
405
|
def set_lens_calibration(
|
|
409
406
|
self,
|
|
@@ -629,6 +626,17 @@ class CameraConfig:
|
|
|
629
626
|
dist_wall = (dist_shore**2 + depth**2) ** 0.5
|
|
630
627
|
return dist_wall
|
|
631
628
|
|
|
629
|
+
def get_extrinsic(self):
|
|
630
|
+
"""Return rotation and translation vector based on control points and intrinsic parameters."""
|
|
631
|
+
# solve rvec and tvec with reduced coordinates, this ensure that the solvepnp solution is stable.
|
|
632
|
+
_, rvec, tvec = cv.solvepnp(self.gcps_reduced, self.gcps["src"], self.camera_matrix, self.dist_coeffs)
|
|
633
|
+
# ensure that rvec and tvec are corrected for the fact that mean gcp location was subtracted
|
|
634
|
+
rvec_cam, tvec_cam = cv.pose_world_to_camera(rvec, tvec)
|
|
635
|
+
tvec_cam += self.gcps_mean
|
|
636
|
+
# transform back to world
|
|
637
|
+
rvec, tvec = cv.pose_world_to_camera(rvec_cam, tvec_cam)
|
|
638
|
+
return rvec, tvec
|
|
639
|
+
|
|
632
640
|
def z_to_h(self, z: float) -> float:
|
|
633
641
|
"""Convert z coordinates of bathymetry to height coordinates in local reference (e.g. staff gauge).
|
|
634
642
|
|
|
@@ -861,39 +869,129 @@ class CameraConfig:
|
|
|
861
869
|
f"a list of lists of 4 coordinates must be given, resulting in (4, "
|
|
862
870
|
f"2) shape. Current shape is {corners.shape} "
|
|
863
871
|
)
|
|
872
|
+
assert self.gcps["z_0"] is not None, "The water level must be set before the bounding box can be established."
|
|
864
873
|
|
|
865
874
|
# get homography
|
|
866
875
|
corners_xyz = self.unproject_points(corners, np.ones(4) * self.gcps["z_0"])
|
|
867
876
|
bbox = cv.get_aoi(corners_xyz, resolution=self.resolution)
|
|
868
877
|
self.bbox = bbox
|
|
869
878
|
|
|
870
|
-
def
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
"""Set lens and distortion parameters.
|
|
879
|
+
def set_bbox_from_width_length(self, points: List[List[float]]):
|
|
880
|
+
"""Establish bbox based on three provided points.
|
|
881
|
+
|
|
882
|
+
The points are provided in the original camera perspective as [col, row] and require that a water level
|
|
883
|
+
has already been set in order to project them in a feasible way.
|
|
876
884
|
|
|
877
|
-
|
|
885
|
+
first point : left bank (seen in downstream direction)
|
|
886
|
+
second point : right bank
|
|
887
|
+
third point : selected upstream or downstream of the two points.
|
|
888
|
+
|
|
889
|
+
The last point defines how large the bounding box is in up-and-downstream direction. A user should attempt to
|
|
890
|
+
choose the first two points roughly in the middle of the intended bounding box. The last point is then
|
|
891
|
+
used to estimate the length perpendicular to the line between the first two points. The bounding box is
|
|
892
|
+
extended in both directions with the same length.
|
|
878
893
|
|
|
879
894
|
Parameters
|
|
880
895
|
----------
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
896
|
+
points : list of lists (3)
|
|
897
|
+
[columns, row] coordinates in original camera perspective without any undistortion applied
|
|
898
|
+
|
|
899
|
+
"""
|
|
900
|
+
assert np.array(points).shape == (3, 2), (
|
|
901
|
+
f"a list of lists of 3 coordinates must be given, resulting in (3, "
|
|
902
|
+
f"2) shape. Current shape is {np.array(points).shape} "
|
|
903
|
+
)
|
|
904
|
+
assert self.gcps["z_0"] is not None, "The water level must be set before the bounding box can be established."
|
|
905
|
+
# get homography
|
|
906
|
+
points_xyz = self.unproject_points(points, np.ones(3) * self.gcps["z_0"])
|
|
907
|
+
bbox = cv.get_aoi(points_xyz, resolution=self.resolution, method="width_length")
|
|
908
|
+
self.bbox = bbox
|
|
884
909
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
910
|
+
def rotate_translate_bbox(self, angle: float = None, xoff: float = None, yoff: float = None):
|
|
911
|
+
"""Rotate and translate the bounding box.
|
|
912
|
+
|
|
913
|
+
Parameters
|
|
914
|
+
----------
|
|
915
|
+
angle : float, optional
|
|
916
|
+
Rotation angle in radians (anti-clockwise) around the center of the bounding box
|
|
917
|
+
xoff : float, optional
|
|
918
|
+
Translation distance in x direction in CRS units
|
|
919
|
+
yoff : float, optional
|
|
920
|
+
Translation distance in y direction in CRS units
|
|
921
|
+
|
|
922
|
+
Returns
|
|
923
|
+
-------
|
|
924
|
+
CameraConfig
|
|
925
|
+
New CameraConfig instance with rotated and translated bounding box
|
|
888
926
|
|
|
889
927
|
"""
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
928
|
+
# Make a deep copy of current config
|
|
929
|
+
new_config = copy.deepcopy(self)
|
|
930
|
+
|
|
931
|
+
# Get the current bbox
|
|
932
|
+
bbox = new_config.bbox
|
|
933
|
+
if bbox is None:
|
|
934
|
+
return new_config
|
|
935
|
+
|
|
936
|
+
# Apply rotation if specified
|
|
937
|
+
if angle is not None:
|
|
938
|
+
print(angle)
|
|
939
|
+
# # Convert to radians
|
|
940
|
+
# angle = np.radians(rotation)
|
|
941
|
+
# Get centroid as origin
|
|
942
|
+
centroid = bbox.centroid
|
|
943
|
+
# Apply rotation around centroid
|
|
944
|
+
bbox = shapely.affinity.rotate(
|
|
945
|
+
bbox,
|
|
946
|
+
angle,
|
|
947
|
+
origin=centroid,
|
|
948
|
+
use_radians=True,
|
|
949
|
+
)
|
|
895
950
|
|
|
896
|
-
|
|
951
|
+
# Now perform translation. Get coordinates of corners
|
|
952
|
+
coords = list(bbox.exterior.coords)
|
|
953
|
+
|
|
954
|
+
# Get unit vectors of x and y directions
|
|
955
|
+
p1 = np.array(coords[0])
|
|
956
|
+
p2 = np.array(coords[1]) # second point
|
|
957
|
+
p3 = np.array(coords[2]) # third point
|
|
958
|
+
|
|
959
|
+
x_vec = p2 - p1
|
|
960
|
+
y_vec = p3 - p2
|
|
961
|
+
|
|
962
|
+
x_vec = x_vec / np.linalg.norm(x_vec)
|
|
963
|
+
y_vec = y_vec / np.linalg.norm(y_vec)
|
|
964
|
+
# Project translations onto these vectors
|
|
965
|
+
dx = 0 if xoff is None else xoff * x_vec[0]
|
|
966
|
+
dy = 0 if xoff is None else xoff * x_vec[1]
|
|
967
|
+
|
|
968
|
+
dx -= 0 if yoff is None else yoff * y_vec[0]
|
|
969
|
+
dy -= 0 if yoff is None else yoff * y_vec[1]
|
|
970
|
+
|
|
971
|
+
# Apply translation
|
|
972
|
+
bbox = shapely.affinity.translate(bbox, xoff=dx, yoff=dy)
|
|
973
|
+
new_config.bbox = bbox
|
|
974
|
+
return new_config
|
|
975
|
+
|
|
976
|
+
def calibrate(
|
|
977
|
+
self,
|
|
978
|
+
):
|
|
979
|
+
"""Calibrate camera parameters using ground control.
|
|
980
|
+
|
|
981
|
+
If nothing provided, they are derived by optimizing pnp fitting together with optimizing the focal length
|
|
982
|
+
and two radial distortion coefficients (k1, k2).
|
|
983
|
+
|
|
984
|
+
You may also provide camera matrix or distortion coefficients, which will only optimize
|
|
985
|
+
the remainder parameters.
|
|
986
|
+
|
|
987
|
+
As a result, the following is updated on the CameraConfig instance:
|
|
988
|
+
- camera_matrix: the 3x3 camera matrix
|
|
989
|
+
- dist_coeffs: the 5x1 distortion coefficients
|
|
990
|
+
- rvec: the 3x1 rotation vector
|
|
991
|
+
- tvec: the 3x1 translation vector
|
|
992
|
+
"""
|
|
993
|
+
if hasattr(self, "gcps") and (self.camera_matrix is None or self.dist_coeffs is None):
|
|
994
|
+
# some calibration is needed, and there are GCPs available for it
|
|
897
995
|
if len(self.gcps["src"]) >= 4:
|
|
898
996
|
self.camera_matrix, self.dist_coeffs, err = cv.optimize_intrinsic(
|
|
899
997
|
self.gcps["src"],
|
|
@@ -902,9 +1000,14 @@ class CameraConfig:
|
|
|
902
1000
|
self.height,
|
|
903
1001
|
self.width,
|
|
904
1002
|
lens_position=self.lens_position,
|
|
905
|
-
camera_matrix=camera_matrix,
|
|
906
|
-
dist_coeffs=dist_coeffs,
|
|
1003
|
+
camera_matrix=self.camera_matrix,
|
|
1004
|
+
dist_coeffs=self.dist_coeffs,
|
|
907
1005
|
)
|
|
1006
|
+
# finally, also derive the rvec and tvec if camera matrix and distortion coefficients are known
|
|
1007
|
+
if self.camera_matrix is not None and self.dist_coeffs is not None:
|
|
1008
|
+
rvec, tvec = self.get_extrinsic()
|
|
1009
|
+
self.rvec = rvec
|
|
1010
|
+
self.tvec = tvec
|
|
908
1011
|
|
|
909
1012
|
def set_gcps(
|
|
910
1013
|
self, src: List[List], dst: List[List], z_0: float, h_ref: Optional[float] = None, crs: Optional[Any] = None
|
|
@@ -146,12 +146,14 @@ class CrossSection:
|
|
|
146
146
|
"""
|
|
147
147
|
# if cross_section is a GeoDataFrame, check if it has a CRS, if yes, convert coordinates to crs of CameraConfig
|
|
148
148
|
if isinstance(cross_section, gpd.GeoDataFrame):
|
|
149
|
-
|
|
149
|
+
crs_cs = getattr(cross_section, "crs", None)
|
|
150
|
+
crs_cam = getattr(camera_config, "crs", None)
|
|
151
|
+
if crs_cs is not None and crs_cam is not None:
|
|
150
152
|
cross_section.to_crs(camera_config.crs, inplace=True)
|
|
151
|
-
elif
|
|
153
|
+
elif crs_cs is not None or crs_cam is not None:
|
|
152
154
|
raise ValueError("if a CRS is used, then both camera_config and cross_section must have a CRS.")
|
|
153
155
|
g = cross_section.geometry
|
|
154
|
-
x, y, z = g.x, g.y, g.z
|
|
156
|
+
x, y, z = g.x.values, g.y.values, g.z.values
|
|
155
157
|
else:
|
|
156
158
|
x, y, z = list(map(list, zip(*cross_section)))
|
|
157
159
|
|
|
@@ -244,17 +246,33 @@ class CrossSection:
|
|
|
244
246
|
diff_xy = np.array(point2_xy) - np.array(point1_xy)
|
|
245
247
|
return np.arctan2(diff_xy[1], diff_xy[0])
|
|
246
248
|
|
|
249
|
+
@property
|
|
250
|
+
def distance_camera(self):
|
|
251
|
+
"""Estimate distance of mean coordinate of cross section to camera position."""
|
|
252
|
+
coord_mean = np.mean(self.cs_linestring.coords, axis=0)
|
|
253
|
+
return np.sum((self.camera_config.estimate_lens_position() - coord_mean) ** 2) ** 0.5
|
|
254
|
+
|
|
247
255
|
@property
|
|
248
256
|
def idx_closest_point(self):
|
|
249
257
|
"""Determine index of point in cross-section, closest to the camera."""
|
|
250
|
-
return self.d.
|
|
258
|
+
return 0 if self.d[0] < self.d[-1] else len(self.d) - 1
|
|
251
259
|
|
|
252
260
|
@property
|
|
253
261
|
def idx_farthest_point(self):
|
|
254
262
|
"""Determine index of point in cross-section, farthest from the camera."""
|
|
255
|
-
return self.d.
|
|
263
|
+
return 0 if self.d[0] > self.d[-1] else len(self.d) - 1
|
|
256
264
|
|
|
257
|
-
|
|
265
|
+
@property
|
|
266
|
+
def within_image(self):
|
|
267
|
+
"""Check if any of the points of the cross section fall inside the image objective."""
|
|
268
|
+
# check if cross section is visible within the image objective
|
|
269
|
+
pix = self.camera_config.project_points(np.array(list(map(list, self.cs_linestring.coords))), within_image=True)
|
|
270
|
+
# check which points fall within the image objective
|
|
271
|
+
within_image = np.all([pix[:, 0] >= 0, pix[:, 0] < 1920, pix[:, 1] >= 0, pix[:, 1] < 1080], axis=0)
|
|
272
|
+
# check if there are any points within the image objective and return result
|
|
273
|
+
return bool(np.any(within_image))
|
|
274
|
+
|
|
275
|
+
def get_cs_waterlevel(self, h: float, sz=False, extend_by=None) -> geometry.LineString:
|
|
258
276
|
"""Retrieve LineString of water surface at cross-section at a given water level.
|
|
259
277
|
|
|
260
278
|
Parameters
|
|
@@ -263,18 +281,44 @@ class CrossSection:
|
|
|
263
281
|
water level [m]
|
|
264
282
|
sz : bool, optional
|
|
265
283
|
If set, return water level line in y-z projection, by default False.
|
|
284
|
+
extend_by : float, optional
|
|
285
|
+
If set, the line will be extended left and right using the defined float in meters
|
|
266
286
|
|
|
267
287
|
Returns
|
|
268
288
|
-------
|
|
269
289
|
geometry.LineString
|
|
270
|
-
horizontal line at water level (2d if sz
|
|
290
|
+
horizontal line at water level (2d if `sz`=True, 3d if `yz`=False)
|
|
271
291
|
|
|
272
292
|
"""
|
|
273
293
|
# get water level in camera config vertical datum
|
|
274
294
|
z = self.camera_config.h_to_z(h)
|
|
275
295
|
if sz:
|
|
276
|
-
|
|
277
|
-
|
|
296
|
+
if extend_by is None:
|
|
297
|
+
s_coords = self.s
|
|
298
|
+
else:
|
|
299
|
+
s_coords = np.concatenate([[-np.abs(extend_by)], self.s, [self.s[-1] + np.abs(extend_by)]])
|
|
300
|
+
return geometry.LineString(zip(s_coords, [z] * len(s_coords)))
|
|
301
|
+
if extend_by is not None:
|
|
302
|
+
alpha = np.arctan((self.x[1] - self.x[0]) / (self.y[1] - self.y[0]))
|
|
303
|
+
x_coords = np.concatenate(
|
|
304
|
+
[
|
|
305
|
+
[self.x[0] - np.cos(alpha) * np.abs(extend_by)],
|
|
306
|
+
self.x,
|
|
307
|
+
[self.x[-1] + np.cos(alpha) * np.abs(extend_by)],
|
|
308
|
+
]
|
|
309
|
+
)
|
|
310
|
+
y_coords = np.concatenate(
|
|
311
|
+
[
|
|
312
|
+
[self.y[0] - np.sin(alpha) * np.abs(extend_by)],
|
|
313
|
+
self.y,
|
|
314
|
+
[self.y[-1] + np.sin(alpha) * np.abs(extend_by)],
|
|
315
|
+
]
|
|
316
|
+
)
|
|
317
|
+
else:
|
|
318
|
+
x_coords = self.x
|
|
319
|
+
y_coords = self.y
|
|
320
|
+
|
|
321
|
+
return geometry.LineString(zip(x_coords, y_coords, [z] * len(x_coords)))
|
|
278
322
|
|
|
279
323
|
def get_csl_point(
|
|
280
324
|
self, h: Optional[float] = None, l: Optional[float] = None, camera: bool = False, swap_y_coords: bool = False
|
|
@@ -586,16 +630,32 @@ class CrossSection:
|
|
|
586
630
|
Wetted surface as a polygon, in Y-Z projection.
|
|
587
631
|
|
|
588
632
|
"""
|
|
589
|
-
wl = self.get_cs_waterlevel(
|
|
633
|
+
wl = self.get_cs_waterlevel(
|
|
634
|
+
h=h, sz=True, extend_by=0.1
|
|
635
|
+
) # extend a small bit to guarantee crossing with the bottom coordinates
|
|
636
|
+
zl = wl.xy[1][0]
|
|
590
637
|
# create polygon by making a union
|
|
591
|
-
|
|
638
|
+
bottom_points = self.cs_points_sz
|
|
639
|
+
# add a point left and right slightly above the level if the level is below the water level
|
|
640
|
+
if bottom_points[0].y < zl:
|
|
641
|
+
bottom_points.insert(0, geometry.Point(bottom_points[0].x, zl + 0.1))
|
|
642
|
+
if bottom_points[-1].y < zl:
|
|
643
|
+
bottom_points.append(geometry.Point(bottom_points[-1].x, zl + 0.1))
|
|
644
|
+
bottom_line = geometry.LineString(bottom_points)
|
|
645
|
+
pol = list(polygonize(wl.union(bottom_line)))
|
|
592
646
|
if len(pol) == 0:
|
|
593
|
-
|
|
647
|
+
# create infinitely small polygon at lowest z coordinate
|
|
648
|
+
lowest_z = min(self.z)
|
|
649
|
+
lowest_s = self.s[list(self.z).index(lowest_z)]
|
|
650
|
+
# make an infinitely small polygon around the lowest point in the cross section
|
|
651
|
+
pol = [geometry.Polygon([(lowest_s, lowest_z)] * 3)]
|
|
594
652
|
elif len(pol) > 1:
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
653
|
+
# detect which polygons have their average z coordinate below the defined water level
|
|
654
|
+
pol = [p for p in pol if p.centroid.xy[1][0] < zl]
|
|
655
|
+
# raise ValueError("Water level is crossed by multiple polygons.")
|
|
656
|
+
# else:
|
|
657
|
+
# pol = pol[0]
|
|
658
|
+
return geometry.MultiPolygon(pol)
|
|
599
659
|
|
|
600
660
|
def get_wetted_surface(self, h: float, camera: bool = False, swap_y_coords=False) -> geometry.Polygon:
|
|
601
661
|
"""Retrieve a wetted surface for a given water level, as a geometry.Polygon.
|
|
@@ -617,13 +677,16 @@ class CrossSection:
|
|
|
617
677
|
|
|
618
678
|
|
|
619
679
|
"""
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
680
|
+
pols = self.get_wetted_surface_sz(h=h)
|
|
681
|
+
pols_proj = []
|
|
682
|
+
for pol in pols.geoms:
|
|
683
|
+
coords = [[self.interp_x_from_s(p[0]), self.interp_y_from_s(p[0]), p[1]] for p in pol.exterior.coords]
|
|
684
|
+
if camera:
|
|
685
|
+
coords_proj = self.camera_config.project_points(coords, swap_y_coords=swap_y_coords)
|
|
686
|
+
pols_proj.append(geometry.Polygon(coords_proj))
|
|
687
|
+
else:
|
|
688
|
+
pols_proj.append(geometry.Polygon(coords))
|
|
689
|
+
return geometry.MultiPolygon(pols_proj)
|
|
627
690
|
|
|
628
691
|
def get_line_of_interest(self, bank: BANK_OPTIONS = "far") -> List[float]:
|
|
629
692
|
"""Retrieve the points of interest within the cross-section for water level detection.
|
|
@@ -670,18 +733,26 @@ class CrossSection:
|
|
|
670
733
|
offset: float = 0.0,
|
|
671
734
|
padding: float = 0.5,
|
|
672
735
|
length: float = 2.0,
|
|
736
|
+
min_z: Optional[float] = None,
|
|
737
|
+
max_z: Optional[float] = None,
|
|
673
738
|
min_samples: int = 50,
|
|
674
739
|
):
|
|
675
740
|
"""Retrieve a histogram score for a given l-value."""
|
|
676
741
|
l = x[0]
|
|
677
|
-
|
|
742
|
+
if min_z is not None:
|
|
743
|
+
if self.interp_z(l) < min_z:
|
|
744
|
+
# return worst score
|
|
745
|
+
return 2.0 + np.abs(self.interp_z(l) - min_z)
|
|
746
|
+
elif max_z is not None:
|
|
747
|
+
if self.interp_z(l) > max_z:
|
|
748
|
+
return 2.0 + np.abs(self.interp_z(l) - max_z)
|
|
678
749
|
pol1 = self.get_csl_pol(l=l, offset=offset, padding=(0, padding), length=length, camera=True)[0]
|
|
679
750
|
pol2 = self.get_csl_pol(l=l, offset=offset, padding=(-padding, 0), length=length, camera=True)[0]
|
|
680
751
|
# get intensity values within polygons
|
|
681
752
|
ints1 = cv.get_polygon_pixels(img, pol1)
|
|
682
753
|
ints2 = cv.get_polygon_pixels(img, pol2)
|
|
683
754
|
if ints1.size < min_samples or ints2.size < min_samples:
|
|
684
|
-
# return a strong penalty score value
|
|
755
|
+
# return a strong penalty score value if there are too few samples
|
|
685
756
|
return 2.0
|
|
686
757
|
_, _, norm_counts1 = _histogram(ints1, normalize=True, bin_size=bin_size)
|
|
687
758
|
bin_centers, bin_edges, norm_counts2 = _histogram(ints2, normalize=True, bin_size=bin_size)
|
|
@@ -857,12 +928,20 @@ class CrossSection:
|
|
|
857
928
|
plt.axes
|
|
858
929
|
|
|
859
930
|
"""
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
931
|
+
try:
|
|
932
|
+
surf = self.get_planar_surface(
|
|
933
|
+
h=h, length=length, offset=offset, swap_y_coords=swap_y_coords, camera=camera
|
|
934
|
+
)
|
|
935
|
+
if camera:
|
|
936
|
+
p = plot_helpers.plot_polygon(surf, ax=ax, label="surface", **kwargs)
|
|
937
|
+
else:
|
|
938
|
+
p = plot_helpers.plot_3d_polygon(surf, ax=ax, label="surface", **kwargs)
|
|
939
|
+
return p.axes
|
|
940
|
+
except Exception:
|
|
941
|
+
warnings.warn(
|
|
942
|
+
"Cannot plot planar surface as there are too many crossings",
|
|
943
|
+
stacklevel=2,
|
|
944
|
+
)
|
|
866
945
|
|
|
867
946
|
def plot_bottom_surface(
|
|
868
947
|
self, length: float = 2.0, offset: float = 0.0, camera: bool = False, ax=None, swap_y_coords=False, **kwargs
|
|
@@ -1003,6 +1082,10 @@ class CrossSection:
|
|
|
1003
1082
|
length: float = 2.0,
|
|
1004
1083
|
padding: float = 0.5,
|
|
1005
1084
|
offset: float = 0.0,
|
|
1085
|
+
min_h: Optional[float] = None,
|
|
1086
|
+
max_h: Optional[float] = None,
|
|
1087
|
+
min_z: Optional[float] = None,
|
|
1088
|
+
max_z: Optional[float] = None,
|
|
1006
1089
|
) -> float:
|
|
1007
1090
|
"""Detect water level optically from provided image.
|
|
1008
1091
|
|
|
@@ -1030,11 +1113,32 @@ class CrossSection:
|
|
|
1030
1113
|
left and right of hypothesized water line at -padding and +padding.
|
|
1031
1114
|
offset : float, optional
|
|
1032
1115
|
perpendicular offset of the waterline from the cross-section [m], by default 0.0
|
|
1116
|
+
min_h : float, optional
|
|
1117
|
+
minimum water level to try detection [m]. If not provided, the minimum water level is taken from the
|
|
1118
|
+
cross section.
|
|
1119
|
+
max_h : float, optional
|
|
1120
|
+
maximum water level to try detection [m]. If not provided, the maximum water level is taken from the
|
|
1121
|
+
cross section.
|
|
1122
|
+
min_z : float, optional
|
|
1123
|
+
same as min_h but using z-coordinates instead of local datum, min_z overrules min_h
|
|
1124
|
+
max_z : float, optional
|
|
1125
|
+
same as max_z but using z-coordinates instead of local datum, max_z overrules max_h
|
|
1033
1126
|
|
|
1034
1127
|
"""
|
|
1035
|
-
|
|
1128
|
+
if min_z is None:
|
|
1129
|
+
if min_h is not None:
|
|
1130
|
+
min_z = self.camera_config.h_to_z(min_h)
|
|
1131
|
+
min_z = np.maximum(min_z, self.z.min())
|
|
1132
|
+
if max_z is None:
|
|
1133
|
+
if max_h is not None:
|
|
1134
|
+
max_z = self.camera_config.h_to_z(max_h)
|
|
1135
|
+
max_z = np.minimum(max_z, self.z.max())
|
|
1136
|
+
if min_z and max_z:
|
|
1137
|
+
if min_z > max_z:
|
|
1138
|
+
raise ValueError("Minimum water level is higher than maximum water level.")
|
|
1139
|
+
|
|
1036
1140
|
if len(img.shape) == 3:
|
|
1037
|
-
# flatten image first
|
|
1141
|
+
# flatten image first if it his a time dimension
|
|
1038
1142
|
img = img.mean(axis=2)
|
|
1039
1143
|
assert (
|
|
1040
1144
|
img.shape[0] == self.camera_config.height
|
|
@@ -1043,12 +1147,13 @@ class CrossSection:
|
|
|
1043
1147
|
img.shape[1] == self.camera_config.width
|
|
1044
1148
|
), f"Image width {img.shape[1]} is not the same as camera_config width {self.camera_config.width}"
|
|
1045
1149
|
# determine the relevant start point if only one is used
|
|
1150
|
+
# import pdb;pdb.set_trace()
|
|
1046
1151
|
l_min, l_max = self.get_line_of_interest(bank=bank)
|
|
1047
1152
|
opt = differential_evolution(
|
|
1048
1153
|
self.get_histogram_score,
|
|
1049
1154
|
popsize=50,
|
|
1050
1155
|
bounds=[(l_min, l_max)],
|
|
1051
|
-
args=(img, bin_size, offset, padding, length),
|
|
1156
|
+
args=(img, bin_size, offset, padding, length, min_z, max_z),
|
|
1052
1157
|
atol=0.01, # one mm
|
|
1053
1158
|
)
|
|
1054
1159
|
z = self.interp_z(opt.x[0])
|