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.
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 _get_cam_mtx(height, width, c=2.0, focal_length=None):
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: upstream-left, downstream-left, downstream-right, upstream-right
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
- polygon = Polygon(dst_corners)
926
- coords = np.array(polygon.exterior.coords)
927
- # estimate the angle of the bounding box
928
- # retrieve average line across AOI
929
- point1 = (coords[0] + coords[3]) / 2
930
- point2 = (coords[1] + coords[2]) / 2
931
- diff = point2 - point1
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=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
- f = x[0] * width # only one parameter to optimize for now, can easily be extended!
1030
- dist_coeffs[0][0] = float(x[1])
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, camera_matrix, dist_coeffs)
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, camera_matrix, dist_coeffs)
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
- if len(dst) == 4:
1058
- bnds_k1 = (-0.0, 0.0)
1059
- bnds_k2 = (-0.0, 0.0)
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
- # bnds_k1 = (-0.2501, -0.25)
1062
- bnds_k1 = (-0.9, 0.9)
1063
- bnds_k2 = (-0.5, 0.5)
1064
- # bnds_k1 = (-0.0, 0.0)
1065
- # bnds_k2 = (-0.0, 0.0)
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=[(float(0.25), float(2)), bnds_k1, bnds_k2],
1202
+ bounds=bounds,
1070
1203
  # bounds=[(1710./width, 1714./width), bnds_k1, bnds_k2],
1071
- args=(src, dst, height, width, c, lens_position, DIST_COEFFS),
1204
+ args=(src, dst, height, width, c, lens_position, camera_matrix, dist_coeffs),
1072
1205
  atol=0.001, # one mm
1073
1206
  )
1074
- camera_matrix = _get_cam_mtx(height, width, focal_length=opt.x[0] * width)
1075
- dist_coeffs = DIST_COEFFS
1076
- dist_coeffs[0][0] = opt.x[1]
1077
- dist_coeffs[1][0] = opt.x[2]
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}")