pyopenrivercam 0.9.2__py3-none-any.whl → 0.9.3__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.9.2.dist-info → pyopenrivercam-0.9.3.dist-info}/METADATA +1 -1
- {pyopenrivercam-0.9.2.dist-info → pyopenrivercam-0.9.3.dist-info}/RECORD +10 -10
- pyorc/__init__.py +1 -1
- pyorc/api/cameraconfig.py +6 -6
- pyorc/api/cross_section.py +266 -10
- pyorc/cv.py +18 -2
- pyorc/plot_helpers.py +4 -2
- {pyopenrivercam-0.9.2.dist-info → pyopenrivercam-0.9.3.dist-info}/WHEEL +0 -0
- {pyopenrivercam-0.9.2.dist-info → pyopenrivercam-0.9.3.dist-info}/entry_points.txt +0 -0
- {pyopenrivercam-0.9.2.dist-info → pyopenrivercam-0.9.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
pyorc/__init__.py,sha256=
|
|
1
|
+
pyorc/__init__.py,sha256=1lhSZ9vOEHMu0j54z2mVnf14JyHHoCSLxAGnh6Lf0hY,523
|
|
2
2
|
pyorc/const.py,sha256=Ia0KRkm-E1lJk4NxQVPDIfN38EBB7BKvxmwIHJrGPUY,2597
|
|
3
|
-
pyorc/cv.py,sha256=
|
|
3
|
+
pyorc/cv.py,sha256=0U7Bwe29tRLSJXIpAxStrwGElheBt54_GslexREcw50,50953
|
|
4
4
|
pyorc/helpers.py,sha256=jed0YyywnpvsZS-8mcA7Lfzn9np1MTlmVLE_PDn2QY0,30454
|
|
5
|
-
pyorc/plot_helpers.py,sha256=
|
|
5
|
+
pyorc/plot_helpers.py,sha256=mlDsiTqd2VMJTvUFOnnAcW_EIOVtChX37F-zISP__-M,1921
|
|
6
6
|
pyorc/project.py,sha256=CGKfICkQEpFRmh_ZeDEfbQ-wefJt7teWJd6B5IPF038,7747
|
|
7
7
|
pyorc/pyorc.sh,sha256=-xOSUNnMAwVbdNkjKNKMZMaBljWsGLhadG-j0DNlJP4,5
|
|
8
8
|
pyorc/sample_data.py,sha256=AhQgYozpbMcSxtNyi6WfHSKHeSRgDQqFhLO0-VrQKiU,3171
|
|
9
9
|
pyorc/api/__init__.py,sha256=k2OQQH4NrtXTuVm23d0g_SX6H5DhnKC9_kDyzJ4dWdk,428
|
|
10
|
-
pyorc/api/cameraconfig.py,sha256=
|
|
11
|
-
pyorc/api/cross_section.py,sha256=
|
|
10
|
+
pyorc/api/cameraconfig.py,sha256=6cEhPGf55H_zLu7K3wKAFz3-zide4jFgddYnycZuZ1c,65449
|
|
11
|
+
pyorc/api/cross_section.py,sha256=eS2tiZ9kMiVlFGBfpz5qTHXAw7Bb6UPURJu4zmAhBjM,75029
|
|
12
12
|
pyorc/api/frames.py,sha256=BnglhmHdbKlIip5tym3x-aICOpQRmm853109A7JkWk8,23189
|
|
13
13
|
pyorc/api/mask.py,sha256=A3TRMqi30L4N491C4FoYY0zvV1GwQ1U31OEkCJp_Nzc,16698
|
|
14
14
|
pyorc/api/orcbase.py,sha256=C23QTKOyxHUafyJsq_t7xn_BzAEvf4DDfzlYAopons8,4189
|
|
@@ -26,8 +26,8 @@ pyorc/service/camera_config.py,sha256=OsRLpe5jd-lu6HT4Vx5wEg554CMS-IKz-q62ir4VbP
|
|
|
26
26
|
pyorc/service/velocimetry.py,sha256=bPI1OdN_fi0gZES08mb7yqCS_4I-lKSZ2JvWSGTRD1E,34434
|
|
27
27
|
pyorc/velocimetry/__init__.py,sha256=5oShoMocCalcCZuIsBqlZlqQuKJgDDBUvXQIo-uqFPA,88
|
|
28
28
|
pyorc/velocimetry/ffpiv.py,sha256=92XDgzCW4mEZ5ow82zV0APOhfDc1OVftBjKqYdw1zzc,17494
|
|
29
|
-
pyopenrivercam-0.9.
|
|
30
|
-
pyopenrivercam-0.9.
|
|
31
|
-
pyopenrivercam-0.9.
|
|
32
|
-
pyopenrivercam-0.9.
|
|
33
|
-
pyopenrivercam-0.9.
|
|
29
|
+
pyopenrivercam-0.9.3.dist-info/entry_points.txt,sha256=Cv_WI2Y6QLnPiNCXGli0gS4WAOAeMoprha1rAR3vdRE,44
|
|
30
|
+
pyopenrivercam-0.9.3.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
|
31
|
+
pyopenrivercam-0.9.3.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
32
|
+
pyopenrivercam-0.9.3.dist-info/METADATA,sha256=253-bK9Du3taufc0fAYU9QnZemPfRqUJm2NjbrFKBAU,12566
|
|
33
|
+
pyopenrivercam-0.9.3.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.9.
|
|
3
|
+
__version__ = "0.9.3"
|
|
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/cameraconfig.py
CHANGED
|
@@ -459,6 +459,7 @@ class CameraConfig:
|
|
|
459
459
|
z_a: Optional[float] = None,
|
|
460
460
|
within_image: Optional[bool] = False,
|
|
461
461
|
expand_exterior=True,
|
|
462
|
+
exterior_split=400,
|
|
462
463
|
) -> Polygon:
|
|
463
464
|
"""Get bounding box.
|
|
464
465
|
|
|
@@ -485,6 +486,8 @@ class CameraConfig:
|
|
|
485
486
|
Set to True to make an attempt to remove parts of the polygon that lie outside of the image field of view
|
|
486
487
|
expand_exterior : bool, optional
|
|
487
488
|
Set to True to expand the corner points to more points. This is particularly useful for plotting purposes.
|
|
489
|
+
exterior_split : int, optional
|
|
490
|
+
Amount of subline segments to split bounding box in
|
|
488
491
|
|
|
489
492
|
Returns
|
|
490
493
|
-------
|
|
@@ -516,7 +519,7 @@ class CameraConfig:
|
|
|
516
519
|
# distortion on image frame, and to plot partial coverage in the real-world coordinates
|
|
517
520
|
coords_expand = np.zeros((0, 2))
|
|
518
521
|
for n in range(0, len(coords) - 1):
|
|
519
|
-
new_coords = np.linspace(coords[n], coords[n + 1],
|
|
522
|
+
new_coords = np.linspace(coords[n], coords[n + 1], exterior_split // 4)
|
|
520
523
|
coords_expand = np.r_[coords_expand, new_coords]
|
|
521
524
|
coords = coords_expand
|
|
522
525
|
if not z_a:
|
|
@@ -915,9 +918,9 @@ class CameraConfig:
|
|
|
915
918
|
angle : float, optional
|
|
916
919
|
Rotation angle in radians (anti-clockwise) around the center of the bounding box
|
|
917
920
|
xoff : float, optional
|
|
918
|
-
Translation distance in x direction in
|
|
921
|
+
Translation distance in x direction in m.
|
|
919
922
|
yoff : float, optional
|
|
920
|
-
Translation distance in y direction in
|
|
923
|
+
Translation distance in y direction in m.
|
|
921
924
|
|
|
922
925
|
Returns
|
|
923
926
|
-------
|
|
@@ -935,9 +938,6 @@ class CameraConfig:
|
|
|
935
938
|
|
|
936
939
|
# Apply rotation if specified
|
|
937
940
|
if angle is not None:
|
|
938
|
-
print(angle)
|
|
939
|
-
# # Convert to radians
|
|
940
|
-
# angle = np.radians(rotation)
|
|
941
941
|
# Get centroid as origin
|
|
942
942
|
centroid = bbox.centroid
|
|
943
943
|
# Apply rotation around centroid
|
pyorc/api/cross_section.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import copy
|
|
5
6
|
import warnings
|
|
6
7
|
from typing import Dict, List, Literal, Optional, Tuple, Union
|
|
7
8
|
|
|
@@ -12,10 +13,11 @@ import numpy as np
|
|
|
12
13
|
from matplotlib import patheffects
|
|
13
14
|
from scipy.interpolate import interp1d
|
|
14
15
|
from scipy.optimize import differential_evolution
|
|
15
|
-
from shapely import affinity, geometry
|
|
16
|
+
from shapely import affinity, force_2d, force_3d, geometry
|
|
17
|
+
from shapely.affinity import rotate, translate
|
|
16
18
|
from shapely.ops import polygonize, split
|
|
17
19
|
|
|
18
|
-
from
|
|
20
|
+
from .cameraconfig import CameraConfig, cv, plot_helpers
|
|
19
21
|
|
|
20
22
|
MODES = Literal["camera", "geographic", "cross_section"]
|
|
21
23
|
PATH_EFFECTS = [
|
|
@@ -36,6 +38,37 @@ WETTED_KWARGS = {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
|
|
41
|
+
def _fit_line(x, y):
|
|
42
|
+
"""Fit straight (xy direction) line to points in a LineString.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
x : list[float]
|
|
47
|
+
x-coordinates of points forming a line
|
|
48
|
+
y : list[float]
|
|
49
|
+
y-coordinates of points forming a line
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
slope, intercept, angle: float, float, float
|
|
54
|
+
line characteristics, angle is in radians
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
# Use PCA to fit the best line
|
|
58
|
+
ps = np.column_stack([x, y])
|
|
59
|
+
centr = ps.mean(axis=0)
|
|
60
|
+
ps_centered = ps - centr
|
|
61
|
+
|
|
62
|
+
# SVD to find principal direction
|
|
63
|
+
_, _, vh = np.linalg.svd(ps_centered)
|
|
64
|
+
direc = vh[0] # First principal component is the line direction
|
|
65
|
+
|
|
66
|
+
# Calculate angle of the line
|
|
67
|
+
ang = np.arctan2(direc[1], direc[0])
|
|
68
|
+
|
|
69
|
+
return centr, direc, ang
|
|
70
|
+
|
|
71
|
+
|
|
39
72
|
def _make_angle_lines(csl_points, angle_perp, length, offset):
|
|
40
73
|
"""Make lines at cross section points, perpendicular to cross section orientation using angle and offset."""
|
|
41
74
|
# move points
|
|
@@ -98,7 +131,7 @@ class CrossSection:
|
|
|
98
131
|
def __repr__(self):
|
|
99
132
|
return str(self.cs_linestring)
|
|
100
133
|
|
|
101
|
-
def __init__(self, camera_config, cross_section: Union[gpd.GeoDataFrame, List[List]]):
|
|
134
|
+
def __init__(self, camera_config: CameraConfig, cross_section: Union[gpd.GeoDataFrame, List[List]]):
|
|
102
135
|
"""Initialize a cross-section representation.
|
|
103
136
|
|
|
104
137
|
The object is geographically aware through the camera configuration.
|
|
@@ -107,7 +140,7 @@ class CrossSection:
|
|
|
107
140
|
|
|
108
141
|
Parameters
|
|
109
142
|
----------
|
|
110
|
-
camera_config :
|
|
143
|
+
camera_config : pyorc.CameraConfig
|
|
111
144
|
Defines the camera configuration, including potential CRS used for processing
|
|
112
145
|
geographic data consistency.
|
|
113
146
|
cross_section : GeoDataFrame or list of lists
|
|
@@ -268,7 +301,15 @@ class CrossSection:
|
|
|
268
301
|
# check if cross section is visible within the image objective
|
|
269
302
|
pix = self.camera_config.project_points(np.array(list(map(list, self.cs_linestring.coords))), within_image=True)
|
|
270
303
|
# check which points fall within the image objective
|
|
271
|
-
within_image = np.all(
|
|
304
|
+
within_image = np.all(
|
|
305
|
+
[
|
|
306
|
+
pix[:, 0] >= 0,
|
|
307
|
+
pix[:, 0] < self.camera_config.width,
|
|
308
|
+
pix[:, 1] >= 0,
|
|
309
|
+
pix[:, 1] < self.camera_config.height,
|
|
310
|
+
],
|
|
311
|
+
axis=0,
|
|
312
|
+
)
|
|
272
313
|
# check if there are any points within the image objective and return result
|
|
273
314
|
return bool(np.any(within_image))
|
|
274
315
|
|
|
@@ -375,6 +416,8 @@ class CrossSection:
|
|
|
375
416
|
raise ValueError(
|
|
376
417
|
"Cross section is not crossed by water level. Check if water level is within the cross section."
|
|
377
418
|
)
|
|
419
|
+
# sort the cross_sz points to go from s=0 to s max.
|
|
420
|
+
cross_sz = sorted(cross_sz, key=lambda p: p.x)
|
|
378
421
|
# find xyz real-world coordinates and turn in to POINT Z list
|
|
379
422
|
cross = [
|
|
380
423
|
geometry.Point(self.interp_x_from_s(c.xy[0]), self.interp_y_from_s(c.xy[0]), c.xy[1]) for c in cross_sz
|
|
@@ -519,7 +562,84 @@ class CrossSection:
|
|
|
519
562
|
polygons = [geometry.Polygon(coords) for coords in csl_pol_coords]
|
|
520
563
|
return polygons
|
|
521
564
|
|
|
522
|
-
def
|
|
565
|
+
def get_bbox_dry_wet(
|
|
566
|
+
self,
|
|
567
|
+
h: float,
|
|
568
|
+
camera: bool = False,
|
|
569
|
+
swap_y_coords: bool = False,
|
|
570
|
+
dry: bool = False,
|
|
571
|
+
expand_exterior: bool = True,
|
|
572
|
+
exterior_split: int = 100,
|
|
573
|
+
):
|
|
574
|
+
"""Get wet (or dry) part of bounding box, by extending the water line perpendicular to the cross section.
|
|
575
|
+
|
|
576
|
+
Parameters
|
|
577
|
+
----------
|
|
578
|
+
h : float
|
|
579
|
+
water level [m]
|
|
580
|
+
camera : bool, optional
|
|
581
|
+
If set, return 2D projected polygon, by default False. Note that any plotting does not provide shading or
|
|
582
|
+
ray tracing hence plotting a 2D polygon is not recommended.
|
|
583
|
+
swap_y_coords : bool, optional
|
|
584
|
+
if set, all y-coordinates are swapped so that they fit on a flipped version of the image. This is useful
|
|
585
|
+
in case you plot on ascending y-coordinate axis background images (default: False)
|
|
586
|
+
dry : bool, optional
|
|
587
|
+
If set, the dry parts will be returned, by default False, thus returning wet.
|
|
588
|
+
expand_exterior : bool, optional
|
|
589
|
+
Set to True to expand the corner points to more points. This is particularly useful for plotting purposes.
|
|
590
|
+
exterior_split : int, optional
|
|
591
|
+
Amount of subline segments to split bounding box in
|
|
592
|
+
|
|
593
|
+
Returns
|
|
594
|
+
-------
|
|
595
|
+
shapely.geometry.MultiPolygon
|
|
596
|
+
multipolygon representing the dry or wet part of the bounding box of the camera config (2d if camera=True,
|
|
597
|
+
3d if camera=False).
|
|
598
|
+
|
|
599
|
+
See Also
|
|
600
|
+
--------
|
|
601
|
+
CrossSection.plot_bbox_dry_wet : Plot the dry and wet parts of the bounding box resulting from this method.
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
"""
|
|
606
|
+
if self.camera_config.bbox is None:
|
|
607
|
+
raise ValueError("CameraConfig must have a bounding box to use this method.")
|
|
608
|
+
z_water = self.camera_config.h_to_z(h) # 92.1 is exactly hitting a local peak
|
|
609
|
+
geom_plan_2d = force_2d(self.get_planar_surface(h=h, length=10000))
|
|
610
|
+
|
|
611
|
+
if dry:
|
|
612
|
+
# dry pols
|
|
613
|
+
pols = force_3d(self.camera_config.bbox.difference(geom_plan_2d), z=z_water)
|
|
614
|
+
else:
|
|
615
|
+
# wet pol(s)
|
|
616
|
+
pols = force_3d(self.camera_config.bbox.intersection(geom_plan_2d), z=z_water)
|
|
617
|
+
if isinstance(pols, geometry.MultiPolygon):
|
|
618
|
+
pols = pols.geoms
|
|
619
|
+
else:
|
|
620
|
+
pols = [pols]
|
|
621
|
+
pols_proj = []
|
|
622
|
+
for pol in pols:
|
|
623
|
+
coords = [[*p] for p in pol.exterior.coords]
|
|
624
|
+
if camera and len(coords) > 0:
|
|
625
|
+
# also split if required
|
|
626
|
+
if expand_exterior:
|
|
627
|
+
coords_expand = np.zeros((0, 3))
|
|
628
|
+
for n in range(0, len(coords) - 1):
|
|
629
|
+
new_coords = np.linspace(coords[n], coords[n + 1], exterior_split // 4)
|
|
630
|
+
coords_expand = np.r_[coords_expand, new_coords]
|
|
631
|
+
# set expanded coords to original variable
|
|
632
|
+
coords = coords_expand
|
|
633
|
+
|
|
634
|
+
coords_proj = self.camera_config.project_points(coords, swap_y_coords=swap_y_coords, within_image=True)
|
|
635
|
+
pols_proj.append(geometry.Polygon(coords_proj))
|
|
636
|
+
else:
|
|
637
|
+
pols_proj.append(geometry.Polygon(coords))
|
|
638
|
+
return geometry.MultiPolygon(pols_proj)
|
|
639
|
+
|
|
640
|
+
def get_bottom_surface(
|
|
641
|
+
self, length: float = 2.0, offset: float = 0.0, camera: bool = False, swap_y_coords: bool = False
|
|
642
|
+
):
|
|
523
643
|
"""Retrieve a bottom surface polygon for the entire cross section, expanded over a length.
|
|
524
644
|
|
|
525
645
|
Returns a 2D Polygon if camera is True, 3D if False. Useful in particular for 3d situation plots.
|
|
@@ -578,7 +698,7 @@ class CrossSection:
|
|
|
578
698
|
|
|
579
699
|
def get_planar_surface(
|
|
580
700
|
self, h: float, length: float = 2.0, offset: float = 0.0, camera: bool = False, swap_y_coords=False
|
|
581
|
-
) -> geometry.Polygon:
|
|
701
|
+
) -> Union[geometry.Polygon, geometry.MultiPolygon]:
|
|
582
702
|
"""Retrieve a planar water surface for a given water level, as a geometry.Polygon.
|
|
583
703
|
|
|
584
704
|
Returns a 2D Polygon if camera is True, 3D if False
|
|
@@ -608,10 +728,50 @@ class CrossSection:
|
|
|
608
728
|
CrossSection.plot_planar_surface : Plot the planar surface resulting from this method.
|
|
609
729
|
|
|
610
730
|
"""
|
|
731
|
+
# Get the water level crossing points (not camera projected for validation)
|
|
732
|
+
csl_points = self.get_csl_point(h=h, camera=False)
|
|
733
|
+
if len(csl_points) < 2:
|
|
734
|
+
raise ValueError(
|
|
735
|
+
f"Cross section must have at least two points to estimate a planar surface ({len(csl_points)} found)."
|
|
736
|
+
)
|
|
611
737
|
wls = self.get_csl_line(h=h, offset=offset, length=length, camera=camera, swap_y_coords=swap_y_coords)
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
738
|
+
|
|
739
|
+
valid_pairs = []
|
|
740
|
+
for p1, p2, l1, l2 in zip(csl_points[0:-1], csl_points[1:], wls[0:-1], wls[1:]):
|
|
741
|
+
# get s-coordinates
|
|
742
|
+
s1 = self.cs_linestring.project(geometry.Point(p1.x, p1.y))
|
|
743
|
+
s2 = self.cs_linestring.project(geometry.Point(p2.x, p2.y))
|
|
744
|
+
|
|
745
|
+
# check bottom elevation at midpoint
|
|
746
|
+
s_mid = (s1 + s2) / 2
|
|
747
|
+
z_bottom_mid = self.interp_z_from_s(s_mid)
|
|
748
|
+
if z_bottom_mid < p1.z:
|
|
749
|
+
valid_pairs.append((l1, l2))
|
|
750
|
+
if len(valid_pairs) == 0:
|
|
751
|
+
raise ValueError(
|
|
752
|
+
"No valid water level crossings found. Check if water level is within the cross section and if "
|
|
753
|
+
"the cross section is crossed by water."
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
# build the polygons
|
|
757
|
+
polygons = []
|
|
758
|
+
for l1, l2 in valid_pairs:
|
|
759
|
+
pol = geometry.Polygon(list(l1.coords) + list(l2.coords[::-1]))
|
|
760
|
+
if pol.is_valid and not pol.is_empty:
|
|
761
|
+
polygons.append(pol)
|
|
762
|
+
|
|
763
|
+
if len(polygons) == 0:
|
|
764
|
+
raise ValueError(
|
|
765
|
+
"No valid polygons found. Check if water level is within the cross section and if "
|
|
766
|
+
"the cross section is crossed by water."
|
|
767
|
+
)
|
|
768
|
+
elif len(polygons) == 1:
|
|
769
|
+
return polygons[0]
|
|
770
|
+
else:
|
|
771
|
+
return geometry.MultiPolygon(polygons)
|
|
772
|
+
# if len(wls) != 2:
|
|
773
|
+
# raise ValueError("Amount of water line crossings must be 2 for a planar surface estimate.")
|
|
774
|
+
# return geometry.Polygon(list(wls[0].coords) + list(wls[1].coords[::-1]))
|
|
615
775
|
|
|
616
776
|
def get_wetted_surface_sz(
|
|
617
777
|
self, h: float, perimeter: bool = False
|
|
@@ -968,6 +1128,35 @@ class CrossSection:
|
|
|
968
1128
|
stacklevel=2,
|
|
969
1129
|
)
|
|
970
1130
|
|
|
1131
|
+
def plot_bbox_dry_wet(
|
|
1132
|
+
self,
|
|
1133
|
+
h: float,
|
|
1134
|
+
camera: bool = False,
|
|
1135
|
+
ax=None,
|
|
1136
|
+
swap_y_coords: bool = False,
|
|
1137
|
+
kwargs_wet: Optional[Dict] = None,
|
|
1138
|
+
kwargs_dry: Optional[Dict] = None,
|
|
1139
|
+
):
|
|
1140
|
+
"""Plot the bounding box of the dry and wet surfaces for a given water level."""
|
|
1141
|
+
if kwargs_wet is None:
|
|
1142
|
+
kwargs_wet = dict(color="b", label="wet")
|
|
1143
|
+
if kwargs_dry is None:
|
|
1144
|
+
kwargs_dry = dict(color="g", label="dry")
|
|
1145
|
+
if ax is None:
|
|
1146
|
+
if camera:
|
|
1147
|
+
ax = plt.axes()
|
|
1148
|
+
else:
|
|
1149
|
+
ax = plt.axes(projection="3d")
|
|
1150
|
+
dry_pols = self.get_bbox_dry_wet(h=h, dry=True, camera=camera, swap_y_coords=swap_y_coords)
|
|
1151
|
+
wet_pols = self.get_bbox_dry_wet(h=h, dry=False, camera=camera, swap_y_coords=swap_y_coords)
|
|
1152
|
+
if camera:
|
|
1153
|
+
_ = plot_helpers.plot_polygon(wet_pols, ax=ax, **kwargs_wet)
|
|
1154
|
+
p = plot_helpers.plot_polygon(dry_pols, ax=ax, **kwargs_dry)
|
|
1155
|
+
else:
|
|
1156
|
+
_ = plot_helpers.plot_3d_polygon(wet_pols, ax=ax, **kwargs_wet)
|
|
1157
|
+
p = plot_helpers.plot_3d_polygon(dry_pols, ax=ax, **kwargs_dry)
|
|
1158
|
+
return p.axes
|
|
1159
|
+
|
|
971
1160
|
def plot_bottom_surface(
|
|
972
1161
|
self, length: float = 2.0, offset: float = 0.0, camera: bool = False, ax=None, swap_y_coords=False, **kwargs
|
|
973
1162
|
) -> mpl.axes.Axes:
|
|
@@ -1099,6 +1288,73 @@ class CrossSection:
|
|
|
1099
1288
|
)
|
|
1100
1289
|
return ax
|
|
1101
1290
|
|
|
1291
|
+
def rotate_translate(self, angle: Optional[float] = None, xoff: float = 0.0, yoff: float = 0.0, zoff: float = 0.0):
|
|
1292
|
+
"""Rotate and translate cross section to match config.
|
|
1293
|
+
|
|
1294
|
+
Parameters
|
|
1295
|
+
----------
|
|
1296
|
+
angle : float, optional
|
|
1297
|
+
Rotation angle in radians (anti-clockwise) around the center of the bounding box
|
|
1298
|
+
xoff : float, optional
|
|
1299
|
+
Translation distance in x direction in m.
|
|
1300
|
+
yoff : float, optional
|
|
1301
|
+
Translation distance in y direction in m.
|
|
1302
|
+
zoff : float, optional
|
|
1303
|
+
Translation distance in z direction in m.
|
|
1304
|
+
|
|
1305
|
+
Returns
|
|
1306
|
+
-------
|
|
1307
|
+
CrossSection
|
|
1308
|
+
New transformed cross section instance.
|
|
1309
|
+
|
|
1310
|
+
"""
|
|
1311
|
+
# Apply rotation if specified
|
|
1312
|
+
if angle is not None:
|
|
1313
|
+
# Get centroid as origin
|
|
1314
|
+
centroid = self.cs_linestring.centroid
|
|
1315
|
+
# Apply rotation around centroid
|
|
1316
|
+
new_line = rotate(
|
|
1317
|
+
self.cs_linestring,
|
|
1318
|
+
angle,
|
|
1319
|
+
origin=centroid,
|
|
1320
|
+
use_radians=True,
|
|
1321
|
+
)
|
|
1322
|
+
else:
|
|
1323
|
+
new_line = copy.deepcopy(self.cs_linestring)
|
|
1324
|
+
# now translate
|
|
1325
|
+
new_line = translate(new_line, xoff=xoff, yoff=yoff, zoff=zoff)
|
|
1326
|
+
# create a new cross section object
|
|
1327
|
+
return CrossSection(self.camera_config, list(new_line.coords))
|
|
1328
|
+
|
|
1329
|
+
def linearize(self):
|
|
1330
|
+
"""Snap cross-section points to a best-fit straight line while preserving Z values.
|
|
1331
|
+
|
|
1332
|
+
The points are projected onto the line, maintaining their relative positions along it.
|
|
1333
|
+
|
|
1334
|
+
Returns
|
|
1335
|
+
-------
|
|
1336
|
+
CrossSection
|
|
1337
|
+
New straightened cross section instance.
|
|
1338
|
+
|
|
1339
|
+
"""
|
|
1340
|
+
# Fit straight line
|
|
1341
|
+
centroid, direction, angle = _fit_line(self.x, self.y)
|
|
1342
|
+
|
|
1343
|
+
# Project each point onto the line closest-distance
|
|
1344
|
+
coords = np.column_stack([self.x, self.y])
|
|
1345
|
+
coords_centered = coords - centroid
|
|
1346
|
+
|
|
1347
|
+
# Project onto the line direction
|
|
1348
|
+
projections = np.dot(coords_centered, direction)
|
|
1349
|
+
|
|
1350
|
+
# Calculate new coordinates on the line
|
|
1351
|
+
new_x = centroid[0] + projections * direction[0]
|
|
1352
|
+
new_y = centroid[1] + projections * direction[1]
|
|
1353
|
+
|
|
1354
|
+
# Create new geometries with Z preserved
|
|
1355
|
+
new_points = [[_x, _y, _z] for _x, _y, _z in zip(new_x, new_y, self.z)]
|
|
1356
|
+
return CrossSection(self.camera_config, new_points)
|
|
1357
|
+
|
|
1102
1358
|
def _preprocess_level_range(
|
|
1103
1359
|
self,
|
|
1104
1360
|
min_h: Optional[float] = None,
|
pyorc/cv.py
CHANGED
|
@@ -1163,11 +1163,17 @@ def optimize_intrinsic(src, dst, height, width, c=2.0, lens_position=None, camer
|
|
|
1163
1163
|
k2 = x[param_nr + 1]
|
|
1164
1164
|
dist_coeffs_sample[0][0] = k1
|
|
1165
1165
|
dist_coeffs_sample[1][0] = k2
|
|
1166
|
+
fx = camera_matrix_sample[0, 2]
|
|
1167
|
+
fy = camera_matrix_sample[1, 2]
|
|
1168
|
+
r_max = np.sqrt(fx**2 + fy**2) * camera_matrix_sample[0, 0]
|
|
1169
|
+
penalty_k1k2 = radial_monotonicity_penalty(k1, k2, r_max)
|
|
1170
|
+
|
|
1166
1171
|
else:
|
|
1167
1172
|
# take the existing distortion coefficients
|
|
1168
1173
|
dist_coeffs_sample = dist_coeffs.copy()
|
|
1169
1174
|
k1 = dist_coeffs_sample[0][0]
|
|
1170
1175
|
k2 = dist_coeffs_sample[1][0]
|
|
1176
|
+
penalty_k1k2 = 0.0
|
|
1171
1177
|
|
|
1172
1178
|
# initialize error
|
|
1173
1179
|
err = 100
|
|
@@ -1195,8 +1201,18 @@ def optimize_intrinsic(src, dst, height, width, c=2.0, lens_position=None, camer
|
|
|
1195
1201
|
cam_err = ((_lens_pos - lens_pos2.flatten()) ** 2).sum() ** 0.5
|
|
1196
1202
|
# TODO: for now camera errors are weighted with 10% needs further investigation
|
|
1197
1203
|
err = float(0.1 * cam_err + gcp_err) if cam_err is not None else gcp_err
|
|
1204
|
+
# penalize non monotonically increasing functions
|
|
1205
|
+
err += 100 * penalty_k1k2
|
|
1198
1206
|
return err # assuming gcp pixel distance is about 5 cm
|
|
1199
1207
|
|
|
1208
|
+
def radial_monotonicity_penalty(k1, k2, r_max):
|
|
1209
|
+
"""Penalty in case radii do not monotonically increase towards the border."""
|
|
1210
|
+
rs = np.linspace(0, r_max, 50)
|
|
1211
|
+
deriv = 1 + 3 * k1 * rs**2 + 5 * k2 * rs**4
|
|
1212
|
+
# penalize negative slope
|
|
1213
|
+
penalty = np.sum(np.clip(-deriv, 0, None))
|
|
1214
|
+
return penalty
|
|
1215
|
+
|
|
1200
1216
|
# determine optimization bounds
|
|
1201
1217
|
bounds = []
|
|
1202
1218
|
if camera_matrix is not None and dist_coeffs is not None:
|
|
@@ -1205,8 +1221,8 @@ def optimize_intrinsic(src, dst, height, width, c=2.0, lens_position=None, camer
|
|
|
1205
1221
|
if camera_matrix is None:
|
|
1206
1222
|
bounds.append([float(0.25), float(2)])
|
|
1207
1223
|
if len(dst) > 4 and dist_coeffs is None:
|
|
1208
|
-
bounds.append([-0.
|
|
1209
|
-
bounds.append([-0.
|
|
1224
|
+
bounds.append([-0.5, 0.5]) # k1
|
|
1225
|
+
bounds.append([-0.1, 0.1]) # k2
|
|
1210
1226
|
else:
|
|
1211
1227
|
# set a warning if dist_coeffs is provided without sufficient ground control
|
|
1212
1228
|
if dist_coeffs:
|
pyorc/plot_helpers.py
CHANGED
|
@@ -22,7 +22,8 @@ def plot_3d_polygon(polygon, ax=None, **kwargs):
|
|
|
22
22
|
"""Plot a shapely.geometry.Polygon or MultiPolygon on matplotlib 3d ax."""
|
|
23
23
|
if isinstance(polygon, geometry.MultiPolygon):
|
|
24
24
|
for pol in polygon.geoms:
|
|
25
|
-
|
|
25
|
+
label = kwargs.pop("label", None)
|
|
26
|
+
p = _plot_3d_pol(pol, ax=ax, label=label, **kwargs)
|
|
26
27
|
else:
|
|
27
28
|
p = _plot_3d_pol(polygon, ax=ax, **kwargs)
|
|
28
29
|
return p
|
|
@@ -34,7 +35,8 @@ def plot_polygon(polygon, ax=None, **kwargs):
|
|
|
34
35
|
ax = plt.axes()
|
|
35
36
|
if isinstance(polygon, geometry.MultiPolygon):
|
|
36
37
|
for pol in polygon.geoms:
|
|
37
|
-
|
|
38
|
+
label = kwargs.pop("label", None)
|
|
39
|
+
patch = plt.Polygon(pol.exterior.coords, label=label, **kwargs)
|
|
38
40
|
p = ax.add_patch(patch)
|
|
39
41
|
else:
|
|
40
42
|
patch = plt.Polygon(polygon.exterior.coords, **kwargs)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|