pyopenrivercam 0.8.5__tar.gz → 0.8.7__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 (144) hide show
  1. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/CHANGELOG.md +37 -0
  2. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/PKG-INFO +4 -4
  3. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/README.md +3 -3
  4. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/__init__.py +1 -1
  5. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/api/cameraconfig.py +161 -69
  6. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/api/cross_section.py +59 -9
  7. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/api/frames.py +19 -58
  8. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/api/plot.py +75 -27
  9. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/cli/cli_elements.py +11 -2
  10. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/cli/cli_utils.py +70 -10
  11. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/cli/main.py +29 -3
  12. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/cv.py +187 -49
  13. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/service/velocimetry.py +236 -174
  14. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyproject.toml +7 -0
  15. pyopenrivercam-0.8.7/sonar-project.properties +18 -0
  16. pyopenrivercam-0.8.5/.github/ISSUE_TEMPLATE/issue-report.md +0 -31
  17. pyopenrivercam-0.8.5/.github/workflows/documentation.yml +0 -60
  18. pyopenrivercam-0.8.5/.github/workflows/pypi-publish.yml +0 -57
  19. pyopenrivercam-0.8.5/.github/workflows/pypitest-publish.yml +0 -60
  20. pyopenrivercam-0.8.5/.github/workflows/tests.yml +0 -61
  21. pyopenrivercam-0.8.5/docs/Makefile +0 -21
  22. pyopenrivercam-0.8.5/docs/_images/ChuoKikuu_GCPs.jpg +0 -0
  23. pyopenrivercam-0.8.5/docs/_images/GCPs_interactive.jpg +0 -0
  24. pyopenrivercam-0.8.5/docs/_images/Geul_GCPs.jpg +0 -0
  25. pyopenrivercam-0.8.5/docs/_images/bbox_interactive.jpg +0 -0
  26. pyopenrivercam-0.8.5/docs/_images/camera_calib.gif +0 -0
  27. pyopenrivercam-0.8.5/docs/_images/example_geul_small.jpg +0 -0
  28. pyopenrivercam-0.8.5/docs/_images/site_schematic_cs.svg +0 -1
  29. pyopenrivercam-0.8.5/docs/_images/site_schematic_planar.svg +0 -1
  30. pyopenrivercam-0.8.5/docs/_images/video_edge.gif +0 -0
  31. pyopenrivercam-0.8.5/docs/_images/video_norm.gif +0 -0
  32. pyopenrivercam-0.8.5/docs/_images/video_norm_proj.gif +0 -0
  33. pyopenrivercam-0.8.5/docs/_images/video_orig.gif +0 -0
  34. pyopenrivercam-0.8.5/docs/_images/video_stable.gif +0 -0
  35. pyopenrivercam-0.8.5/docs/_images/video_unstable.gif +0 -0
  36. pyopenrivercam-0.8.5/docs/_images/wark_cam_config.jpg +0 -0
  37. pyopenrivercam-0.8.5/docs/_images/wark_cam_config_persp.jpg +0 -0
  38. pyopenrivercam-0.8.5/docs/_images/wark_cam_persp.jpg +0 -0
  39. pyopenrivercam-0.8.5/docs/_images/wark_discharge.jpg +0 -0
  40. pyopenrivercam-0.8.5/docs/_images/wark_streamplot.jpg +0 -0
  41. pyopenrivercam-0.8.5/docs/_static/logo.png +0 -0
  42. pyopenrivercam-0.8.5/docs/_static/logo.svg +0 -1544
  43. pyopenrivercam-0.8.5/docs/_static/orc_favicon.svg +0 -220
  44. pyopenrivercam-0.8.5/docs/_static/orc_logo_bw.svg +0 -179
  45. pyopenrivercam-0.8.5/docs/_static/orc_logo_color.svg +0 -182
  46. pyopenrivercam-0.8.5/docs/_static/orc_logo_grey.svg +0 -179
  47. pyopenrivercam-0.8.5/docs/_static/theme-localdevices.css +0 -31
  48. pyopenrivercam-0.8.5/docs/_templates/accessor_method.rst +0 -6
  49. pyopenrivercam-0.8.5/docs/api.rst +0 -412
  50. pyopenrivercam-0.8.5/docs/conf.py +0 -136
  51. pyopenrivercam-0.8.5/docs/index.rst +0 -87
  52. pyopenrivercam-0.8.5/docs/installation.rst +0 -114
  53. pyopenrivercam-0.8.5/docs/intro.rst +0 -47
  54. pyopenrivercam-0.8.5/docs/make.bat +0 -35
  55. pyopenrivercam-0.8.5/docs/ngwerere.jpg +0 -0
  56. pyopenrivercam-0.8.5/docs/quickstart.rst +0 -45
  57. pyopenrivercam-0.8.5/docs/requirements.txt +0 -12
  58. pyopenrivercam-0.8.5/docs/user-guide/api.rst +0 -28
  59. pyopenrivercam-0.8.5/docs/user-guide/camera_config/api.rst +0 -39
  60. pyopenrivercam-0.8.5/docs/user-guide/camera_config/api_bbox.rst +0 -50
  61. pyopenrivercam-0.8.5/docs/user-guide/camera_config/api_gcps.rst +0 -78
  62. pyopenrivercam-0.8.5/docs/user-guide/camera_config/api_geo.rst +0 -50
  63. pyopenrivercam-0.8.5/docs/user-guide/camera_config/api_lens_calib.rst +0 -21
  64. pyopenrivercam-0.8.5/docs/user-guide/camera_config/cam_config.py +0 -41
  65. pyopenrivercam-0.8.5/docs/user-guide/camera_config/cli.rst +0 -30
  66. pyopenrivercam-0.8.5/docs/user-guide/camera_config/cli_bbox.rst +0 -27
  67. pyopenrivercam-0.8.5/docs/user-guide/camera_config/cli_gcps.rst +0 -79
  68. pyopenrivercam-0.8.5/docs/user-guide/camera_config/cli_geo.rst +0 -18
  69. pyopenrivercam-0.8.5/docs/user-guide/camera_config/index.rst +0 -465
  70. pyopenrivercam-0.8.5/docs/user-guide/cli.rst +0 -126
  71. pyopenrivercam-0.8.5/docs/user-guide/cross_section/cross_section.jpg +0 -0
  72. pyopenrivercam-0.8.5/docs/user-guide/cross_section/index.rst +0 -323
  73. pyopenrivercam-0.8.5/docs/user-guide/cross_section/polygon_samples.jpg +0 -0
  74. pyopenrivercam-0.8.5/docs/user-guide/cross_section/pyplots/cross.py +0 -21
  75. pyopenrivercam-0.8.5/docs/user-guide/cross_section/wrong_estimate.jpg +0 -0
  76. pyopenrivercam-0.8.5/docs/user-guide/frames/frames.ipynb +0 -296
  77. pyopenrivercam-0.8.5/docs/user-guide/frames/index.rst +0 -211
  78. pyopenrivercam-0.8.5/docs/user-guide/index.rst +0 -98
  79. pyopenrivercam-0.8.5/docs/user-guide/plot/index.rst +0 -182
  80. pyopenrivercam-0.8.5/docs/user-guide/plot/plot_example.jpg +0 -0
  81. pyopenrivercam-0.8.5/docs/user-guide/plot/plot_example.py +0 -56
  82. pyopenrivercam-0.8.5/docs/user-guide/plot/plot_example.yml +0 -26
  83. pyopenrivercam-0.8.5/docs/user-guide/transect/index.rst +0 -113
  84. pyopenrivercam-0.8.5/docs/user-guide/velocimetry/index.rst +0 -388
  85. pyopenrivercam-0.8.5/docs/user-guide/video/index.rst +0 -307
  86. pyopenrivercam-0.8.5/envs/pyorc-dev.yml +0 -37
  87. pyopenrivercam-0.8.5/envs/pyorc-test.yml +0 -40
  88. pyopenrivercam-0.8.5/examples/01_Camera_Configuration_single_video.ipynb +0 -289
  89. pyopenrivercam-0.8.5/examples/02_Process_velocimetry.ipynb +0 -289
  90. pyopenrivercam-0.8.5/examples/03_Plotting_and_masking_velocimetry_results.ipynb +0 -341
  91. pyopenrivercam-0.8.5/examples/04_Extracting_crosssection_velocities_and_discharge.ipynb +0 -321
  92. pyopenrivercam-0.8.5/examples/05_Camera_calibration.ipynb +0 -247
  93. pyopenrivercam-0.8.5/examples/06_Estimating_water_levels_optically.ipynb +0 -299
  94. pyopenrivercam-0.8.5/examples/camera_calib/camera_calib_720p.mkv +0 -0
  95. pyopenrivercam-0.8.5/examples/geul/dk_cam_config.json +0 -104
  96. pyopenrivercam-0.8.5/examples/geul/dk_control.mp4 +0 -0
  97. pyopenrivercam-0.8.5/examples/ngwerere/cross_section1.geojson +0 -33
  98. pyopenrivercam-0.8.5/examples/ngwerere/cross_section2.geojson +0 -31
  99. pyopenrivercam-0.8.5/examples/ngwerere/ngwerere.json +0 -84
  100. pyopenrivercam-0.8.5/examples/ngwerere/ngwerere.yml +0 -91
  101. pyopenrivercam-0.8.5/examples/ngwerere/ngwerere_20191103.mp4 +0 -0
  102. pyopenrivercam-0.8.5/examples/ngwerere/ngwerere_cross_section.csv +0 -28
  103. pyopenrivercam-0.8.5/examples/ngwerere/ngwerere_cross_section_2.csv +0 -26
  104. pyopenrivercam-0.8.5/examples/ngwerere/ngwerere_gcps.geojson +0 -11
  105. pyopenrivercam-0.8.5/examples/ngwerere/ngwerere_masked.nc +0 -0
  106. pyopenrivercam-0.8.5/examples/ngwerere/ngwerere_piv.nc +0 -0
  107. pyopenrivercam-0.8.5/examples/ngwerere/ngwerere_test.yml +0 -68
  108. pyopenrivercam-0.8.5/tests/__init__.py +0 -0
  109. pyopenrivercam-0.8.5/tests/conftest.py +0 -443
  110. pyopenrivercam-0.8.5/tests/test_cameraconfig.py +0 -277
  111. pyopenrivercam-0.8.5/tests/test_cli.py +0 -238
  112. pyopenrivercam-0.8.5/tests/test_cross_section.py +0 -395
  113. pyopenrivercam-0.8.5/tests/test_frames.py +0 -155
  114. pyopenrivercam-0.8.5/tests/test_mask.py +0 -59
  115. pyopenrivercam-0.8.5/tests/test_plot_helpers.py +0 -73
  116. pyopenrivercam-0.8.5/tests/test_sample_data.py +0 -9
  117. pyopenrivercam-0.8.5/tests/test_transect.py +0 -68
  118. pyopenrivercam-0.8.5/tests/test_velocimetry.py +0 -42
  119. pyopenrivercam-0.8.5/tests/test_video.py +0 -70
  120. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/.gitignore +0 -0
  121. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/.pre-commit-config.yaml +0 -0
  122. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/Dockerfile +0 -0
  123. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/LICENSE +0 -0
  124. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/TRADEMARK.md +0 -0
  125. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/environment.yml +0 -0
  126. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/api/__init__.py +0 -0
  127. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/api/mask.py +0 -0
  128. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/api/orcbase.py +0 -0
  129. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/api/transect.py +0 -0
  130. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/api/velocimetry.py +0 -0
  131. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/api/video.py +0 -0
  132. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/cli/__init__.py +0 -0
  133. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/cli/log.py +0 -0
  134. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/const.py +0 -0
  135. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/helpers.py +0 -0
  136. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/plot_helpers.py +0 -0
  137. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/project.py +0 -0
  138. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/pyorc.sh +0 -0
  139. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/sample_data.py +0 -0
  140. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/service/__init__.py +0 -0
  141. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/service/camera_config.py +0 -0
  142. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/velocimetry/__init__.py +0 -0
  143. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/velocimetry/ffpiv.py +0 -0
  144. {pyopenrivercam-0.8.5 → pyopenrivercam-0.8.7}/pyorc/velocimetry/openpiv.py +0 -0
@@ -1,3 +1,40 @@
1
+ ## [0.8.7] = 2025-06-30
2
+ ### Added
3
+ - CLI option `--cross_wl`
4
+ - A new 3-point bounding box estimation, ideal for selecting a bounding box in strongly oblique cases. First select left
5
+ bank, then right bank, then a point up- or downstream from the selected line for the size of the bounding box.
6
+ - `rvec` and `tvec` are written to camera configuration after fitting.
7
+ ### Changed
8
+ - CLI option `--cross_wl` now replaces `--cross` for optical water level estimation. `--cross` is only used for
9
+ discharge calculation
10
+ - Plotting in camera objective is accelerated
11
+ - water level detection with `CrossSection.detect_water_level` can now consume either `min_z` and `max_z` (for levels
12
+ in the original coordinate system) or `min_h` and `max_h` (for levels using the local `h_ref` datum if provided).
13
+ These minimum and maximum levels can be used to pre-condition the level range that the algoithm will seek in.
14
+ - Pose fitting can now be performed with a pre-defined camera matrix and set of distortion coefficients. This is very
15
+ useful when a user has already pre-calibrated these parameters. It will improve the fit of the `rvec` and `tvec`,
16
+ i.e. rotation and translation vectors.
17
+ - Changed code coverage reporting from Codecov to sonarqube for code coverage reports. Codecov caused issues with
18
+ numba code.
19
+ ### Deprecated
20
+ ### Removed
21
+ ### Fixed
22
+ - incorrect estimation of optical water level if `bank="near"` was used. This resulted in only a smaller portion of the
23
+ cross section being used. Now the full nearby side is used.
24
+
25
+ ## [0.8.6] = 2025-05-16
26
+ ### Added
27
+ - added options `--k1`, `--k2` and `--focal_length` to command line interface for cases where
28
+ focal length, and distortion coefficients are already known
29
+ ### Changed
30
+ - optimization of intrinsics can now also be done with partly already known data. If k1 and k2 are known
31
+ these can be passed as camera
32
+ - Debug messaging increased in `service.velocimetry`.
33
+ ### Deprecated
34
+ ### Removed
35
+ ### Fixed
36
+
37
+
1
38
  ## [0.8.5] = 2025-03-25
2
39
  ### Added
3
40
  - option `--cross` can now also be provided at service level. Only relevant
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyopenrivercam
3
- Version: 0.8.5
3
+ Version: 0.8.7
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
  [![PyPI](https://badge.fury.io/py/pyopenrivercam.svg)](https://pypi.org/project/pyopenrivercam)
80
80
  [![Conda-Forge](https://anaconda.org/conda-forge/pyopenrivercam/badges/version.svg)](https://anaconda.org/conda-forge/pyopenrivercam)
81
- [![codecov](https://codecov.io/gh/localdevices/pyorc/branch/main/graph/badge.svg?token=0740LBNK6J)](https://codecov.io/gh/localdevices/pyorc)
81
+ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=localdevices_pyorc&metric=coverage)](https://sonarcloud.io/summary/new_code?id=localdevices_pyorc)
82
+ [![python](https://img.shields.io/pypi/pyversions/pyopenrivercam?color=%2376519B)](https://pypi.org/project/pyopenrivercam/)
82
83
  [![docs_latest](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://localdevices.github.io/pyorc/latest)
83
84
  [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/localdevices/pyorc.git/main?labpath=examples)
84
85
  [![License](https://img.shields.io/github/license/localdevices/pyorc?style=flat)](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
  [![PyPI](https://badge.fury.io/py/pyopenrivercam.svg)](https://pypi.org/project/pyopenrivercam)
10
10
  [![Conda-Forge](https://anaconda.org/conda-forge/pyopenrivercam/badges/version.svg)](https://anaconda.org/conda-forge/pyopenrivercam)
11
- [![codecov](https://codecov.io/gh/localdevices/pyorc/branch/main/graph/badge.svg?token=0740LBNK6J)](https://codecov.io/gh/localdevices/pyorc)
11
+ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=localdevices_pyorc&metric=coverage)](https://sonarcloud.io/summary/new_code?id=localdevices_pyorc)
12
+ [![python](https://img.shields.io/pypi/pyversions/pyopenrivercam?color=%2376519B)](https://pypi.org/project/pyopenrivercam/)
12
13
  [![docs_latest](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://localdevices.github.io/pyorc/latest)
13
14
  [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/localdevices/pyorc.git/main?labpath=examples)
14
15
  [![License](https://img.shields.io/github/license/localdevices/pyorc?style=flat)](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.5"
3
+ __version__ = "0.8.7"
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
@@ -47,7 +47,6 @@ class CameraConfig:
47
47
  lens_position: Optional[List[float]] = None,
48
48
  corners: Optional[List[List[float]]] = None,
49
49
  gcps: Optional[Dict[str, Union[List, float]]] = None,
50
- lens_pars: Optional[Dict[str, float]] = None,
51
50
  calibration_video: Optional[str] = None,
52
51
  is_nadir: Optional[bool] = False,
53
52
  stabilize: Optional[List[List]] = None,
@@ -90,10 +89,6 @@ class CameraConfig:
90
89
  the same vertical reference as the measured bathymetry and other survey points,
91
90
  "crs": int, str or CRS object, CRS in which "dst" points are measured. If None, a local coordinate system is
92
91
  assumed (e.g. from spirit level).
93
- lens_pars : dict, optional
94
- Lens parameters, containing: "k1": float, barrel lens distortion parameter (default: 0.),
95
- "c": float, optical center (default: 2.),
96
- "focal_length": float, focal length (default: width of image frame)
97
92
  calibration_video : str, optional
98
93
  local path to video file containing a checkerboard pattern. Must be 9x6 if called directly, otherwise use
99
94
  ``.calibrate_camera`` explicitly and provide ``chessboard_size`` explicitly. When used, an automated camera
@@ -120,6 +115,8 @@ class CameraConfig:
120
115
  self.height = height
121
116
  self.width = width
122
117
  self.is_nadir = is_nadir
118
+ self.camera_matrix = camera_matrix
119
+ self.dist_coeffs = dist_coeffs
123
120
  self.rvec = rvec
124
121
  self.tvec = tvec
125
122
  if crs is not None:
@@ -137,19 +134,14 @@ class CameraConfig:
137
134
  self.lens_position = None
138
135
  if gcps is not None:
139
136
  self.set_gcps(**gcps)
140
- if camera_matrix is None or dist_coeffs is None:
141
- if self.is_nadir:
142
- # with nadir, no perspective can be constructed, hence, camera matrix and dist coeffs will be set
143
- # to default values
144
- self.camera_matrix = cv._get_cam_mtx(self.height, self.width)
145
- self.dist_coeffs = cv.DIST_COEFFS
146
- # camera pars are incomplete and need to be derived
147
- else:
148
- self.set_intrinsic(camera_matrix=camera_matrix, lens_pars=lens_pars)
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
149
143
  else:
150
- # camera matrix and dist coeffs can also be set hard, this overrules the lens_pars option
151
- self.camera_matrix = camera_matrix
152
- self.dist_coeffs = dist_coeffs
144
+ self.calibrate()
153
145
  if calibration_video is not None:
154
146
  self.set_lens_calibration(calibration_video, plot=False)
155
147
  if bbox is not None:
@@ -201,6 +193,27 @@ class CameraConfig:
201
193
  def dist_coeffs(self, dist_coeffs):
202
194
  self._dist_coeffs = dist_coeffs.tolist() if isinstance(dist_coeffs, np.ndarray) else dist_coeffs
203
195
 
196
+ @property
197
+ def focal_length(self):
198
+ """Get focal length."""
199
+ if not self.camera_matrix:
200
+ return None
201
+ return self.camera_matrix[0][0]
202
+
203
+ @property
204
+ def k1(self):
205
+ """Get first distortion coefficient."""
206
+ if not self.dist_coeffs:
207
+ return None
208
+ return self.dist_coeffs[0]
209
+
210
+ @property
211
+ def k2(self):
212
+ """Get second distortion coefficient."""
213
+ if not self.dist_coeffs:
214
+ return None
215
+ return self.dist_coeffs[1]
216
+
204
217
  @property
205
218
  def gcps_dest(self):
206
219
  """Get destination coordinates of GCPs.
@@ -308,12 +321,12 @@ class CameraConfig:
308
321
  tvec_cam += self.gcps_mean
309
322
  # transform back to world
310
323
  rvec, tvec = cv.pose_world_to_camera(rvec_cam, tvec_cam)
311
- return _, rvec, tvec
324
+ return rvec, tvec
312
325
 
313
326
  @property
314
327
  def rvec(self):
315
328
  """Return rvec from precise N point solution."""
316
- return self.pnp[1].tolist() if self._rvec is None else self._rvec
329
+ return self.pnp[0].tolist() if self._rvec is None else self._rvec
317
330
 
318
331
  @rvec.setter
319
332
  def rvec(self, _rvec):
@@ -383,11 +396,11 @@ class CameraConfig:
383
396
  @property
384
397
  def tvec(self):
385
398
  """Return tvec from precise N point solution."""
386
- return self.pnp[2].tolist() if self._tvec is None else self._tvec
399
+ return self.pnp[1].tolist() if self._tvec is None else self._tvec
387
400
 
388
401
  @tvec.setter
389
402
  def tvec(self, _tvec):
390
- self._tvec = _tvec.tolist if isinstance(_tvec, np.ndarray) else _tvec
403
+ self._tvec = _tvec.tolist() if isinstance(_tvec, np.ndarray) else _tvec
391
404
 
392
405
  def set_lens_calibration(
393
406
  self,
@@ -613,6 +626,17 @@ class CameraConfig:
613
626
  dist_wall = (dist_shore**2 + depth**2) ** 0.5
614
627
  return dist_wall
615
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
+
616
640
  def z_to_h(self, z: float) -> float:
617
641
  """Convert z coordinates of bathymetry to height coordinates in local reference (e.g. staff gauge).
618
642
 
@@ -845,39 +869,129 @@ class CameraConfig:
845
869
  f"a list of lists of 4 coordinates must be given, resulting in (4, "
846
870
  f"2) shape. Current shape is {corners.shape} "
847
871
  )
872
+ assert self.gcps["z_0"] is not None, "The water level must be set before the bounding box can be established."
848
873
 
849
874
  # get homography
850
875
  corners_xyz = self.unproject_points(corners, np.ones(4) * self.gcps["z_0"])
851
876
  bbox = cv.get_aoi(corners_xyz, resolution=self.resolution)
852
877
  self.bbox = bbox
853
878
 
854
- def set_intrinsic(
855
- self,
856
- camera_matrix: Optional[List[List]] = None,
857
- dist_coeffs: Optional[List[List]] = None,
858
- lens_pars: Optional[Dict[str, float]] = None,
859
- ):
860
- """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.
861
881
 
862
- If not provided, they are derived by optimizing pnp fitting together with optimizing the focal length.
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.
884
+
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.
863
893
 
864
894
  Parameters
865
895
  ----------
866
- camera_matrix : Optional[List[List]]
867
- A defined camera matrix to set as intrinsic parameters. If not provided, it will use default values or
868
- those derived from ground control points (GCPs) if available.
896
+ points : list of lists (3)
897
+ [columns, row] coordinates in original camera perspective without any undistortion applied
869
898
 
870
- dist_coeffs : Optional[List[List]]
871
- Distortion coefficients to be used for the camera. If not provided, it will use default values or those
872
- derived from GCPs if available.
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
873
909
 
874
- lens_pars : Optional[Dict[str, float]]
875
- Lens parameters to be set. These will override any default settings or those derived from GCPs if provided.
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
876
926
 
877
927
  """
878
- # first set a default estimate from pose if 3D gcps are available
879
- self.set_lens_pars() # default parameters use width of frame
880
- if hasattr(self, "gcps"):
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
+ )
950
+
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
881
995
  if len(self.gcps["src"]) >= 4:
882
996
  self.camera_matrix, self.dist_coeffs, err = cv.optimize_intrinsic(
883
997
  self.gcps["src"],
@@ -886,36 +1000,14 @@ class CameraConfig:
886
1000
  self.height,
887
1001
  self.width,
888
1002
  lens_position=self.lens_position,
1003
+ camera_matrix=self.camera_matrix,
1004
+ dist_coeffs=self.dist_coeffs,
889
1005
  )
890
- if lens_pars is not None:
891
- # override with lens parameter set by user
892
- self.set_lens_pars(**lens_pars)
893
- if camera_matrix is not None and dist_coeffs is not None:
894
- # override with
895
- self.camera_matrix = camera_matrix
896
- self.dist_coeffs = dist_coeffs
897
-
898
- def set_lens_pars(self, k1: Optional[float] = 0.0, c: Optional[float] = 2.0, focal_length: Optional[float] = None):
899
- """Set the lens parameters of the given CameraConfig.
900
-
901
- Parameters
902
- ----------
903
- k1 : float, optional
904
- lens curvature [-], zero (default) means no curvature
905
- c : float, optional
906
- optical centre [1/n], where n is the fraction of the lens diameter, 2.0 (default) means in the
907
- centre.
908
- focal_length : float, optional
909
- focal length [mm], typical values could be 2.8, or 4 (default).
910
-
911
-
912
- """
913
- assert isinstance(k1, (int, float)), "k1 must be a float"
914
- assert isinstance(c, (int, float)), "c must be a float"
915
- if focal_length is not None:
916
- assert isinstance(focal_length, (int, float, None)), "f must be a float"
917
- self.dist_coeffs = cv._get_dist_coefs(k1)
918
- self.camera_matrix = cv._get_cam_mtx(self.height, self.width, c=c, focal_length=focal_length)
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
919
1011
 
920
1012
  def set_gcps(
921
1013
  self, src: List[List], dst: List[List], z_0: float, h_ref: Optional[float] = None, crs: Optional[Any] = None
@@ -151,7 +151,7 @@ class CrossSection:
151
151
  elif cross_section.crs is not None or camera_config.crs is not None:
152
152
  raise ValueError("if a CRS is used, then both camera_config and cross_section must have a CRS.")
153
153
  g = cross_section.geometry
154
- x, y, z = g.x, g.y, g.z
154
+ x, y, z = g.x.values, g.y.values, g.z.values
155
155
  else:
156
156
  x, y, z = list(map(list, zip(*cross_section)))
157
157
 
@@ -244,15 +244,31 @@ class CrossSection:
244
244
  diff_xy = np.array(point2_xy) - np.array(point1_xy)
245
245
  return np.arctan2(diff_xy[1], diff_xy[0])
246
246
 
247
+ @property
248
+ def distance_camera(self):
249
+ """Estimate distance of mean coordinate of cross section to camera position."""
250
+ coord_mean = np.mean(self.cs_linestring.coords, axis=0)
251
+ return np.sum((self.camera_config.estimate_lens_position() - coord_mean) ** 2) ** 0.5
252
+
247
253
  @property
248
254
  def idx_closest_point(self):
249
255
  """Determine index of point in cross-section, closest to the camera."""
250
- return self.d.argmin()
256
+ return 0 if self.d[0] < self.d[-1] else len(self.d) - 1
251
257
 
252
258
  @property
253
259
  def idx_farthest_point(self):
254
260
  """Determine index of point in cross-section, farthest from the camera."""
255
- return self.d.argmax()
261
+ return 0 if self.d[0] > self.d[-1] else len(self.d) - 1
262
+
263
+ @property
264
+ def within_image(self):
265
+ """Check if any of the points of the cross section fall inside the image objective."""
266
+ # check if cross section is visible within the image objective
267
+ pix = self.camera_config.project_points(np.array(list(map(list, self.cs_linestring.coords))), within_image=True)
268
+ # check which points fall within the image objective
269
+ within_image = np.all([pix[:, 0] >= 0, pix[:, 0] < 1920, pix[:, 1] >= 0, pix[:, 1] < 1080], axis=0)
270
+ # check if there are any points within the image objective and return result
271
+ return bool(np.any(within_image))
256
272
 
257
273
  def get_cs_waterlevel(self, h: float, sz=False) -> geometry.LineString:
258
274
  """Retrieve LineString of water surface at cross-section at a given water level.
@@ -670,18 +686,26 @@ class CrossSection:
670
686
  offset: float = 0.0,
671
687
  padding: float = 0.5,
672
688
  length: float = 2.0,
689
+ min_z: Optional[float] = None,
690
+ max_z: Optional[float] = None,
673
691
  min_samples: int = 50,
674
692
  ):
675
693
  """Retrieve a histogram score for a given l-value."""
676
694
  l = x[0]
677
- # print(l)
695
+ if min_z is not None:
696
+ if self.interp_z(l) < min_z:
697
+ # return worst score
698
+ return 2.0 + np.abs(self.interp_z(l) - min_z)
699
+ elif max_z is not None:
700
+ if self.interp_z(l) > max_z:
701
+ return 2.0 + np.abs(self.interp_z(l) - max_z)
678
702
  pol1 = self.get_csl_pol(l=l, offset=offset, padding=(0, padding), length=length, camera=True)[0]
679
703
  pol2 = self.get_csl_pol(l=l, offset=offset, padding=(-padding, 0), length=length, camera=True)[0]
680
704
  # get intensity values within polygons
681
705
  ints1 = cv.get_polygon_pixels(img, pol1)
682
706
  ints2 = cv.get_polygon_pixels(img, pol2)
683
707
  if ints1.size < min_samples or ints2.size < min_samples:
684
- # return a strong penalty score value
708
+ # return a strong penalty score value if there are too few samples
685
709
  return 2.0
686
710
  _, _, norm_counts1 = _histogram(ints1, normalize=True, bin_size=bin_size)
687
711
  bin_centers, bin_edges, norm_counts2 = _histogram(ints2, normalize=True, bin_size=bin_size)
@@ -986,7 +1010,7 @@ class CrossSection:
986
1010
  lens_position_xy = self.camera_config.estimate_lens_position()[0:2]
987
1011
  dists = [((p.x - lens_position_xy[0]) ** 2 + (p.y - lens_position_xy[1]) ** 2) ** 0.5 for p in points]
988
1012
  points = self.get_csl_point(h=h, camera=True, swap_y_coords=swap_y_coords) # find camera positions
989
- x, y = points[np.argmin(dists)].xy
1013
+ x, y = points[np.argmax(dists)].xy
990
1014
  x, y = float(x[0]), float(y[0])
991
1015
 
992
1016
  # only plot text in 2D camera perspective at farthest point
@@ -1003,6 +1027,10 @@ class CrossSection:
1003
1027
  length: float = 2.0,
1004
1028
  padding: float = 0.5,
1005
1029
  offset: float = 0.0,
1030
+ min_h: Optional[float] = None,
1031
+ max_h: Optional[float] = None,
1032
+ min_z: Optional[float] = None,
1033
+ max_z: Optional[float] = None,
1006
1034
  ) -> float:
1007
1035
  """Detect water level optically from provided image.
1008
1036
 
@@ -1030,11 +1058,32 @@ class CrossSection:
1030
1058
  left and right of hypothesized water line at -padding and +padding.
1031
1059
  offset : float, optional
1032
1060
  perpendicular offset of the waterline from the cross-section [m], by default 0.0
1061
+ min_h : float, optional
1062
+ minimum water level to try detection [m]. If not provided, the minimum water level is taken from the
1063
+ cross section.
1064
+ max_h : float, optional
1065
+ maximum water level to try detection [m]. If not provided, the maximum water level is taken from the
1066
+ cross section.
1067
+ min_z : float, optional
1068
+ same as min_h but using z-coordinates instead of local datum, min_z overrules min_h
1069
+ max_z : float, optional
1070
+ same as max_z but using z-coordinates instead of local datum, max_z overrules max_h
1033
1071
 
1034
1072
  """
1035
- """Attempt to detect the water line level along the cross-section, using a provided pre-treated image."""
1073
+ if min_z is None:
1074
+ if min_h is not None:
1075
+ min_z = self.camera_config.h_to_z(min_h)
1076
+ min_z = np.maximum(min_z, self.z.min())
1077
+ if max_z is None:
1078
+ if max_h is not None:
1079
+ max_z = self.camera_config.h_to_z(max_h)
1080
+ max_z = np.minimum(max_z, self.z.max())
1081
+ if min_z and max_z:
1082
+ if min_z > max_z:
1083
+ raise ValueError("Minimum water level is higher than maximum water level.")
1084
+
1036
1085
  if len(img.shape) == 3:
1037
- # flatten image first
1086
+ # flatten image first if it his a time dimension
1038
1087
  img = img.mean(axis=2)
1039
1088
  assert (
1040
1089
  img.shape[0] == self.camera_config.height
@@ -1043,12 +1092,13 @@ class CrossSection:
1043
1092
  img.shape[1] == self.camera_config.width
1044
1093
  ), f"Image width {img.shape[1]} is not the same as camera_config width {self.camera_config.width}"
1045
1094
  # determine the relevant start point if only one is used
1095
+ # import pdb;pdb.set_trace()
1046
1096
  l_min, l_max = self.get_line_of_interest(bank=bank)
1047
1097
  opt = differential_evolution(
1048
1098
  self.get_histogram_score,
1049
1099
  popsize=50,
1050
1100
  bounds=[(l_min, l_max)],
1051
- args=(img, bin_size, offset, padding, length),
1101
+ args=(img, bin_size, offset, padding, length, min_z, max_z),
1052
1102
  atol=0.01, # one mm
1053
1103
  )
1054
1104
  z = self.interp_z(opt.x[0])