pyopenrivercam 0.8.5__py3-none-any.whl → 0.8.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyopenrivercam-0.8.5.dist-info → pyopenrivercam-0.8.7.dist-info}/METADATA +4 -4
- {pyopenrivercam-0.8.5.dist-info → pyopenrivercam-0.8.7.dist-info}/RECORD +15 -15
- pyorc/__init__.py +1 -1
- pyorc/api/cameraconfig.py +161 -69
- pyorc/api/cross_section.py +59 -9
- pyorc/api/frames.py +19 -58
- pyorc/api/plot.py +75 -27
- pyorc/cli/cli_elements.py +11 -2
- pyorc/cli/cli_utils.py +70 -10
- pyorc/cli/main.py +29 -3
- pyorc/cv.py +187 -49
- pyorc/service/velocimetry.py +236 -174
- {pyopenrivercam-0.8.5.dist-info → pyopenrivercam-0.8.7.dist-info}/WHEEL +0 -0
- {pyopenrivercam-0.8.5.dist-info → pyopenrivercam-0.8.7.dist-info}/entry_points.txt +0 -0
- {pyopenrivercam-0.8.5.dist-info → pyopenrivercam-0.8.7.dist-info}/licenses/LICENSE +0 -0
pyorc/cv.py
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
4
|
import os
|
|
5
|
+
import warnings
|
|
5
6
|
|
|
6
7
|
import cv2
|
|
7
8
|
import numpy as np
|
|
8
9
|
import rasterio
|
|
9
10
|
from scipy import optimize
|
|
10
11
|
from shapely.affinity import rotate
|
|
11
|
-
from shapely.geometry import LineString, Polygon
|
|
12
|
+
from shapely.geometry import LineString, Point, Polygon
|
|
12
13
|
from tqdm import tqdm
|
|
13
14
|
|
|
14
15
|
from . import helpers
|
|
@@ -87,6 +88,56 @@ def _combine_m(m1, m2):
|
|
|
87
88
|
return m_combi
|
|
88
89
|
|
|
89
90
|
|
|
91
|
+
def _get_aoi_corners(dst_corners, resolution=None):
|
|
92
|
+
polygon = Polygon(dst_corners)
|
|
93
|
+
coords = np.array(polygon.exterior.coords)
|
|
94
|
+
# estimate the angle of the bounding box
|
|
95
|
+
# retrieve average line across AOI
|
|
96
|
+
point1 = (coords[0] + coords[3]) / 2
|
|
97
|
+
point2 = (coords[1] + coords[2]) / 2
|
|
98
|
+
diff = point2 - point1
|
|
99
|
+
angle = np.arctan2(diff[1], diff[0])
|
|
100
|
+
# rotate the polygon over this angle to get a proper bounding box
|
|
101
|
+
polygon_rotate = rotate(polygon, -angle, origin=tuple(dst_corners[0]), use_radians=True)
|
|
102
|
+
|
|
103
|
+
xmin, ymin, xmax, ymax = polygon_rotate.bounds
|
|
104
|
+
if resolution is not None:
|
|
105
|
+
xmin = helpers.round_to_multiple(xmin, resolution)
|
|
106
|
+
xmax = helpers.round_to_multiple(xmax, resolution)
|
|
107
|
+
ymin = helpers.round_to_multiple(ymin, resolution)
|
|
108
|
+
ymax = helpers.round_to_multiple(ymax, resolution)
|
|
109
|
+
|
|
110
|
+
bbox_coords = [(xmin, ymax), (xmax, ymax), (xmax, ymin), (xmin, ymin), (xmin, ymax)]
|
|
111
|
+
bbox = Polygon(bbox_coords)
|
|
112
|
+
# now rotate back
|
|
113
|
+
bbox = rotate(bbox, angle, origin=tuple(dst_corners[0]), use_radians=True)
|
|
114
|
+
return bbox
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _get_aoi_width_length(dst_corners):
|
|
118
|
+
points = [Point(x, y) for x, y, _ in dst_corners]
|
|
119
|
+
linecross = LineString([points[0], points[1]])
|
|
120
|
+
# linecross = LineString(dst_corners[0:2])
|
|
121
|
+
length = np.abs(_get_perpendicular_distance(points[-1], linecross))
|
|
122
|
+
point1 = np.array(dst_corners[0][0:2])
|
|
123
|
+
point2 = np.array(dst_corners[1][0:2])
|
|
124
|
+
diff = np.array(point2 - point1)
|
|
125
|
+
angle = np.arctan2(diff[1], diff[0])
|
|
126
|
+
|
|
127
|
+
# compute xy distance from line to other line making up the bounding box
|
|
128
|
+
xy_diff = np.array([np.sin(-angle) * length, np.cos(angle) * length])
|
|
129
|
+
points_pol = np.array([point1 - xy_diff, point1 + xy_diff, point2 + xy_diff, point2 - xy_diff])
|
|
130
|
+
# always make sure the order of the points of upstream-left, downstream-left, downstream-right, upstream-right
|
|
131
|
+
# if length <= 0:
|
|
132
|
+
# # negative length means the selected length is selected upstream of left-right cross section
|
|
133
|
+
# points_pol = np.array([point1 + xy_diff, point1, point2, point2 + xy_diff])
|
|
134
|
+
# else:
|
|
135
|
+
# # postive means it is selected downstream of left-right cross section
|
|
136
|
+
# points_pol = np.array([point1, point1 + xy_diff, point2 + xy_diff, point2])
|
|
137
|
+
|
|
138
|
+
return Polygon(points_pol)
|
|
139
|
+
|
|
140
|
+
|
|
90
141
|
def _smooth(img, stride):
|
|
91
142
|
"""Blur image through gaussian smoothing.
|
|
92
143
|
|
|
@@ -151,7 +202,53 @@ def _get_dist_coefs(k1):
|
|
|
151
202
|
return dist
|
|
152
203
|
|
|
153
204
|
|
|
154
|
-
def
|
|
205
|
+
def _get_perpendicular_distance(point, line):
|
|
206
|
+
"""Calculate perpendicular distance from point to line.
|
|
207
|
+
|
|
208
|
+
Line is extended if perpendicular distance is larger than the distance to the endpoints.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
point : shapely.geometry.Point
|
|
213
|
+
x, y coordinates of point
|
|
214
|
+
line : shapely.geometry.LineString
|
|
215
|
+
line to calculate distance to
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
float
|
|
220
|
+
perpendicular distance from point to line
|
|
221
|
+
|
|
222
|
+
"""
|
|
223
|
+
# Get coordinates of line endpoints
|
|
224
|
+
p1 = np.array(line.coords[0])
|
|
225
|
+
p2 = np.array(line.coords[1])
|
|
226
|
+
# Convert point to numpy array
|
|
227
|
+
p3 = np.array(point.coords[0])
|
|
228
|
+
|
|
229
|
+
# Calculate line vector
|
|
230
|
+
line_vector = p2 - p1
|
|
231
|
+
# Calculate vector from point to line start
|
|
232
|
+
point_vector = p3 - p1
|
|
233
|
+
|
|
234
|
+
# Calculate unit vector of line
|
|
235
|
+
unit_line = line_vector / np.linalg.norm(line_vector)
|
|
236
|
+
|
|
237
|
+
# Calculate projection length
|
|
238
|
+
projection_length = np.dot(point_vector, unit_line)
|
|
239
|
+
|
|
240
|
+
# Calculate perpendicular vector
|
|
241
|
+
perpendicular_vector = point_vector - projection_length * unit_line
|
|
242
|
+
perpendicular_distance = np.linalg.norm(perpendicular_vector)
|
|
243
|
+
|
|
244
|
+
# Use cross product to calculate side
|
|
245
|
+
cross_product = np.cross(line_vector, point_vector)
|
|
246
|
+
|
|
247
|
+
# Determine the sign of the perpendicular distance
|
|
248
|
+
return perpendicular_distance if cross_product > 0 else -perpendicular_distance
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def get_cam_mtx(height, width, c=2.0, focal_length=None):
|
|
155
252
|
"""Compute camera matrix based on the given parameters for height, width, scaling factor, and focal length.
|
|
156
253
|
|
|
157
254
|
Parameters
|
|
@@ -906,15 +1003,19 @@ def get_ortho(img, M, shape, flags=cv2.INTER_AREA):
|
|
|
906
1003
|
return cv2.warpPerspective(img, M, shape, flags=flags)
|
|
907
1004
|
|
|
908
1005
|
|
|
909
|
-
def get_aoi(dst_corners, resolution=None):
|
|
910
|
-
"""Get rectangular AOI from 4 user defined points within frames.
|
|
1006
|
+
def get_aoi(dst_corners, resolution=None, method="corners"):
|
|
1007
|
+
"""Get rectangular AOI from 3 or 4 user defined points within frames.
|
|
911
1008
|
|
|
912
1009
|
Parameters
|
|
913
1010
|
----------
|
|
914
1011
|
dst_corners : np.ndarray
|
|
915
|
-
corners of aoi, in order:
|
|
1012
|
+
corners of aoi, with `method="width_length"` in order: left-bank, right-bank, up/downstream point,
|
|
1013
|
+
with `method="corners"` in order: upstream-left, downstream-left, downstream-right, upstream-right.
|
|
916
1014
|
resolution : float
|
|
917
1015
|
resolution of intended reprojection, used to round the bbox to a whole number of intended pixels
|
|
1016
|
+
method : str
|
|
1017
|
+
can be "corners" or "width_length". With "corners", the AOI is defined by the four corners of the rectangle.
|
|
1018
|
+
With "width" length, the AOI is defined by the width (2 points) and length (1 point) of the rectangle.
|
|
918
1019
|
|
|
919
1020
|
Returns
|
|
920
1021
|
-------
|
|
@@ -922,27 +1023,14 @@ def get_aoi(dst_corners, resolution=None):
|
|
|
922
1023
|
bounding box of aoi (with rotated affine)
|
|
923
1024
|
|
|
924
1025
|
"""
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
angle = np.arctan2(diff[1], diff[0])
|
|
933
|
-
# rotate the polygon over this angle to get a proper bounding box
|
|
934
|
-
polygon_rotate = rotate(polygon, -angle, origin=tuple(dst_corners[0]), use_radians=True)
|
|
935
|
-
xmin, ymin, xmax, ymax = polygon_rotate.bounds
|
|
936
|
-
if resolution is not None:
|
|
937
|
-
xmin = helpers.round_to_multiple(xmin, resolution)
|
|
938
|
-
xmax = helpers.round_to_multiple(xmax, resolution)
|
|
939
|
-
ymin = helpers.round_to_multiple(ymin, resolution)
|
|
940
|
-
ymax = helpers.round_to_multiple(ymax, resolution)
|
|
1026
|
+
if method == "corners":
|
|
1027
|
+
bbox = _get_aoi_corners(dst_corners, resolution)
|
|
1028
|
+
elif method == "width_length":
|
|
1029
|
+
bbox = _get_aoi_width_length(dst_corners)
|
|
1030
|
+
|
|
1031
|
+
else:
|
|
1032
|
+
raise ValueError("method must be 'corners' or 'width_length'")
|
|
941
1033
|
|
|
942
|
-
bbox_coords = [(xmin, ymax), (xmax, ymax), (xmax, ymin), (xmin, ymin), (xmin, ymax)]
|
|
943
|
-
bbox = Polygon(bbox_coords)
|
|
944
|
-
# now rotate back
|
|
945
|
-
bbox = rotate(bbox, angle, origin=tuple(dst_corners[0]), use_radians=True)
|
|
946
1034
|
return bbox
|
|
947
1035
|
|
|
948
1036
|
|
|
@@ -958,7 +1046,7 @@ def get_polygon_pixels(img, pol, reverse_y=False):
|
|
|
958
1046
|
return img[mask == 255]
|
|
959
1047
|
|
|
960
1048
|
|
|
961
|
-
def optimize_intrinsic(src, dst, height, width, c=2.0, lens_position=None):
|
|
1049
|
+
def optimize_intrinsic(src, dst, height, width, c=2.0, lens_position=None, camera_matrix=None, dist_coeffs=None):
|
|
962
1050
|
"""Optimize the intrinsic parameters of a camera model.
|
|
963
1051
|
|
|
964
1052
|
The function finds optimal intrinsic camera parameters, including focal length and distortion coefficients, by
|
|
@@ -980,6 +1068,11 @@ def optimize_intrinsic(src, dst, height, width, c=2.0, lens_position=None):
|
|
|
980
1068
|
Center parameter of the camera matrix.
|
|
981
1069
|
lens_position : array_like, optional
|
|
982
1070
|
The assumed position of the lens in the 3D space.
|
|
1071
|
+
camera_matrix : Optional[List[List]]
|
|
1072
|
+
Predefined camera matrix. If not provided focal length will be fitted and camera matrix returned
|
|
1073
|
+
dist_coeffs : Optional[List[List]]
|
|
1074
|
+
Distortion coefficients to be used for the camera. If not provided, the first two (k1, k2)
|
|
1075
|
+
distortion coefficients are fitted on data.
|
|
983
1076
|
|
|
984
1077
|
Returns
|
|
985
1078
|
-------
|
|
@@ -989,7 +1082,7 @@ def optimize_intrinsic(src, dst, height, width, c=2.0, lens_position=None):
|
|
|
989
1082
|
|
|
990
1083
|
"""
|
|
991
1084
|
|
|
992
|
-
def error_intrinsic(x, src, dst, height, width, c=2.0, lens_position=None, dist_coeffs=
|
|
1085
|
+
def error_intrinsic(x, src, dst, height, width, c=2.0, lens_position=None, camera_matrix=None, dist_coeffs=None):
|
|
993
1086
|
"""Compute the reprojection error for the intrinsic parameters of a camera model.
|
|
994
1087
|
|
|
995
1088
|
This function optimizes for the focal length and first two distortion coefficients based on the source and
|
|
@@ -1014,6 +1107,8 @@ def optimize_intrinsic(src, dst, height, width, c=2.0, lens_position=None):
|
|
|
1014
1107
|
center parameter of camera matrix.
|
|
1015
1108
|
lens_position : array_like, optional
|
|
1016
1109
|
The assumed position of the lens in the 3D space.
|
|
1110
|
+
camera_matrix : array_like, optional
|
|
1111
|
+
camera matrix [3x3]
|
|
1017
1112
|
dist_coeffs : array_like, optional
|
|
1018
1113
|
Distortion coefficients.
|
|
1019
1114
|
|
|
@@ -1024,24 +1119,43 @@ def optimize_intrinsic(src, dst, height, width, c=2.0, lens_position=None):
|
|
|
1024
1119
|
the camera position error if the lens position is provided.
|
|
1025
1120
|
|
|
1026
1121
|
"""
|
|
1122
|
+
param_nr = 0
|
|
1123
|
+
# set the parameters
|
|
1124
|
+
if camera_matrix is None:
|
|
1125
|
+
f = x[param_nr] * width
|
|
1126
|
+
camera_matrix_sample = get_cam_mtx(height, width, c=c, focal_length=f)
|
|
1127
|
+
param_nr += 1
|
|
1128
|
+
else:
|
|
1129
|
+
# take the existing camera matrix
|
|
1130
|
+
camera_matrix_sample = camera_matrix.copy()
|
|
1131
|
+
if dist_coeffs is None:
|
|
1132
|
+
dist_coeffs_sample = DIST_COEFFS.copy()
|
|
1133
|
+
k1 = x[param_nr]
|
|
1134
|
+
k2 = x[param_nr + 1]
|
|
1135
|
+
dist_coeffs_sample[0][0] = k1
|
|
1136
|
+
dist_coeffs_sample[1][0] = k2
|
|
1137
|
+
else:
|
|
1138
|
+
# take the existing distortion coefficients
|
|
1139
|
+
dist_coeffs_sample = dist_coeffs.copy()
|
|
1140
|
+
k1 = dist_coeffs_sample[0][0]
|
|
1141
|
+
k2 = dist_coeffs_sample[1][0]
|
|
1142
|
+
|
|
1143
|
+
# initialize error
|
|
1027
1144
|
err = 100
|
|
1028
1145
|
cam_err = None
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
dist_coeffs[1][0] = float(x[2])
|
|
1032
|
-
# dist_coeffs[4][0] = float(x[3])
|
|
1033
|
-
# dist_coeffs[3][0] = float(x[4])
|
|
1146
|
+
|
|
1147
|
+
# reduce problem space to centered around gcp average
|
|
1034
1148
|
coord_mean = np.array(dst).mean(axis=0)
|
|
1035
1149
|
_dst = np.float64(np.array(dst) - coord_mean)
|
|
1036
1150
|
zs = np.zeros(4) if len(_dst[0]) == 2 else np.array(_dst)[:, -1]
|
|
1037
1151
|
if lens_position is not None:
|
|
1038
1152
|
_lens_pos = np.array(lens_position) - coord_mean
|
|
1039
1153
|
|
|
1040
|
-
camera_matrix = _get_cam_mtx(height, width, c=c, focal_length=f)
|
|
1041
|
-
success, rvec, tvec = solvepnp(_dst, src,
|
|
1154
|
+
# camera_matrix = _get_cam_mtx(height, width, c=c, focal_length=f)
|
|
1155
|
+
success, rvec, tvec = solvepnp(_dst, src, camera_matrix_sample, dist_coeffs_sample)
|
|
1042
1156
|
if success:
|
|
1043
1157
|
# estimate destination locations from pose
|
|
1044
|
-
dst_est = unproject_points(src, zs, rvec, tvec,
|
|
1158
|
+
dst_est = unproject_points(src, zs, rvec, tvec, camera_matrix_sample, dist_coeffs_sample)
|
|
1045
1159
|
# src_est = np.array([list(point[0]) for point in src_est])
|
|
1046
1160
|
dist_xy = np.array(_dst)[:, 0:2] - np.array(dst_est)[:, 0:2]
|
|
1047
1161
|
dist = (dist_xy**2).sum(axis=1) ** 0.5
|
|
@@ -1054,27 +1168,51 @@ def optimize_intrinsic(src, dst, height, width, c=2.0, lens_position=None):
|
|
|
1054
1168
|
err = float(0.1 * cam_err + gcp_err) if cam_err is not None else gcp_err
|
|
1055
1169
|
return err # assuming gcp pixel distance is about 5 cm
|
|
1056
1170
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1171
|
+
# determine optimization bounds
|
|
1172
|
+
bounds = []
|
|
1173
|
+
if camera_matrix is not None and dist_coeffs is not None:
|
|
1174
|
+
# both are already known, so nothing to do
|
|
1175
|
+
return camera_matrix, dist_coeffs, None
|
|
1176
|
+
if camera_matrix is None:
|
|
1177
|
+
bounds.append([float(0.25), float(2)])
|
|
1178
|
+
if len(dst) > 4 and dist_coeffs is None:
|
|
1179
|
+
bounds.append([-0.9, 0.9]) # k1
|
|
1180
|
+
bounds.append([-0.5, 0.5]) # k2
|
|
1060
1181
|
else:
|
|
1061
|
-
#
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1182
|
+
# set a warning if dist_coeffs is provided without sufficient ground control
|
|
1183
|
+
if dist_coeffs:
|
|
1184
|
+
warnings.warn(
|
|
1185
|
+
"You are trying to optimize distortion coefficients with only 4 GCPs. "
|
|
1186
|
+
"This would lead to overfitting, setting distortion coefficients to zero.",
|
|
1187
|
+
stacklevel=2,
|
|
1188
|
+
)
|
|
1189
|
+
dist_coeffs = DIST_COEFFS.copy()
|
|
1190
|
+
# if len(dst) == 4:
|
|
1191
|
+
# bnds_k1 = (-0.0, 0.0)
|
|
1192
|
+
# bnds_k2 = (-0.0, 0.0)
|
|
1193
|
+
# else:
|
|
1194
|
+
# # bnds_k1 = (-0.2501, -0.25)
|
|
1195
|
+
# bnds_k1 = (-0.9, 0.9)
|
|
1196
|
+
# bnds_k2 = (-0.5, 0.5)
|
|
1197
|
+
# bnds_k1 = (-0.0, 0.0)
|
|
1198
|
+
# bnds_k2 = (-0.0, 0.0)
|
|
1066
1199
|
opt = optimize.differential_evolution(
|
|
1067
1200
|
error_intrinsic,
|
|
1068
1201
|
# bounds=[(float(0.25), float(2)), bnds_k1],#, (-0.5, 0.5)],
|
|
1069
|
-
bounds=
|
|
1202
|
+
bounds=bounds,
|
|
1070
1203
|
# bounds=[(1710./width, 1714./width), bnds_k1, bnds_k2],
|
|
1071
|
-
args=(src, dst, height, width, c, lens_position,
|
|
1204
|
+
args=(src, dst, height, width, c, lens_position, camera_matrix, dist_coeffs),
|
|
1072
1205
|
atol=0.001, # one mm
|
|
1073
1206
|
)
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1207
|
+
param_nr = 0
|
|
1208
|
+
if camera_matrix is None:
|
|
1209
|
+
camera_matrix = get_cam_mtx(height, width, focal_length=opt.x[param_nr] * width)
|
|
1210
|
+
# move to next parameter
|
|
1211
|
+
param_nr += 1
|
|
1212
|
+
if dist_coeffs is None:
|
|
1213
|
+
dist_coeffs = DIST_COEFFS
|
|
1214
|
+
dist_coeffs[0][0] = opt.x[param_nr]
|
|
1215
|
+
dist_coeffs[1][0] = opt.x[param_nr + 1]
|
|
1078
1216
|
# dist_coeffs[4][0] = opt.x[3]
|
|
1079
1217
|
# dist_coeffs[3][0] = opt.x[4]
|
|
1080
1218
|
# print(f"CAMERA MATRIX: {camera_matrix}")
|