1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
| import numpy as np import cv2 from shapely.geometry import Polygon, LineString from shapely.affinity import rotate import rasterio import argparse
def get_gcp_mean(dst): return np.array(dst).mean(axis=0)
def set_bbox_from_corners(corners, resolution, dst): """ Establish bbox based on a set of camera perspective corner points Assign corner coordinates to camera configuration
Parameters ---------- corners : list of lists (4) [columns, row] coordinates in camera perspective
""" assert (np.array(corners).shape == (4, 2)), f"a list of lists of 4 coordinates must be given, resulting in (4, " \ f"2) shape. Current shape is {corners.shape} "
M_gcp = get_M() bbox = get_aoi(M_gcp, corners, resolution=0.05) bbox_xy = np.array(bbox.exterior.coords) gcp_mean = np.array(dst).mean(axis=0) bbox_xy += gcp_mean[0:2] bbox = Polygon(bbox_xy) return bbox, bbox_xy[0:4]
def get_aoi(M, src_corners, resolution=None): """Get rectangular AOI from 4 user defined points within frames.
Parameters ---------- src : list of tuples (col, row) pairs of ground control points dst : list of tuples projected (x, y) coordinates of ground control points src_corners : dict with 4 (x,y) tuples names "up_left", "down_left", "up_right", "down_right", source corners
Returns ------- aoi : shapely.geometry.Polygon bounding box of aoi (rotated) """ _src_corners = np.array(src_corners) assert(_src_corners.shape==(4, 2)), f"a list of lists of 4 coordinates must be given, resulting in (4, 2) shape. " \ f"Current shape is {src_corners.shape} " _dst_corners = cv2.perspectiveTransform(np.float32([_src_corners]), M)[0] polygon = Polygon(_dst_corners) coords = np.array(polygon.exterior.coords) point1 = (coords[0] + coords[3]) / 2 point2 = (coords[1] + coords[2]) / 2 diff = point2 - point1 angle = np.arctan2(diff[1], diff[0]) polygon_rotate = rotate( polygon, -angle, origin=tuple(_dst_corners[0]), use_radians=True ) xmin, ymin, xmax, ymax = polygon_rotate.bounds if resolution is not None: xmin = round_to_multiple(xmin, resolution) xmax = round_to_multiple(xmax, resolution) ymin = round_to_multiple(ymin, resolution) ymax = round_to_multiple(ymax, resolution) bbox_coords = [(xmin, ymax), (xmax, ymax), (xmax, ymin), (xmin, ymin), (xmin, ymax)] bbox = Polygon(bbox_coords) bbox = rotate(bbox, angle, origin=tuple(_dst_corners[0]), use_radians=True) return bbox
def round_to_multiple(number, multiple): return multiple * round(number / multiple)
def get_bbox(bbox, dst, camera=False, h_a=None, redistort=False): """
Parameters ---------- camera : bool, optional If set, the bounding box will be returned as row and column coordinates in the camera perspective. In this case ``h_a`` may be set to provide the right water level, to estimate the bounding box for. h_a : float, optional If set with ``camera=True``, then the bbox coordinates will be transformed to the camera perspective, using h_a as a present water level. In case a video with higher (lower) water levels is used, this will result in a different perspective plane than the control video. redistort : bool, optional If set in combination with ``camera``, the bbox will be redistorted in the camera objective using the distortion coefficients and camera matrix. Not used in orthorectification because this occurs by default on already undistorted images.
Returns ------- A bounding box, that in the used CRS is perfectly rectangular, and aligned in the up/downstream direction. It can (and certainly will) be rotated with respect to a typical bbox with xlim, ylim coordinates. If user sets ``camera=True`` then the geographical bounding box will be converted into a camera perspective, using the homography belonging to the available ground control points and current water level.
This can then be used to reconstruct the grid for velocimetry calculations. """ bbox = bbox if camera: coords = np.array(bbox.exterior.coords) M = get_M(reverse=True, h_a=h_a) gcp_mean = np.array(dst).mean(axis=0) coords -= gcp_mean[0:2] corners = cv2.perspectiveTransform(np.float32([coords]), M)[0]
bbox = Polygon(corners) return bbox
def get_M(h_a=None, to_bbox_grid=False, reverse=False): """Establish a transformation matrix for a certain actual water level `h_a`. This is done by mapping where the ground control points, measured at `h_ref` will end up with new water level `h_a`, given the lens position.
Parameters ---------- h_a : float, optional actual water level [m] (Default: None) to_bbox_grid : bool, optional if set, the M will be computed in row, column values of the target bbox, with set resolution reverse : bool, optional if True, the reverse matrix is prepared, which can be used to transform projected coordinates back to the original camera perspective. (Default: False)
Returns ------- M : np.ndarray 2x3 transformation matrix
""" if to_bbox_grid: dst_a = transform_to_bbox( get_dst_a(h_a), bbox, resolution ) dst_a = np.array(dst_a) else: dst_a = get_dst_a(h_a) if gcp_dims == 2 else dst
gcp_mean = np.array(dst).mean(axis=0) dst_a -= gcp_mean if dst_a.shape[-1] == 3: z_a = get_z_a(h_a) gcp_mean = np.array(dst).mean(axis=0) z_a -= gcp_mean[-1] return get_M_3D( src=src, dst=dst_a, camera_matrix=camera_matrix, dist_coeffs=dist_coeffs, z=z_a, reverse=reverse ) else: return get_M_2D( src=src, dst=dst_a, reverse=reverse )
def get_M_2D(src, dst, reverse=False): """ Retrieve homography matrix for between (4) src and (4) dst points with only x, y coordinates (no z)
Parameters ---------- src : list of lists [x, y] with source coordinates, typically cols and rows in image dst : list of lists [x, y] with target coordinates after reprojection, can e.g. be in crs [m] reverse : bool, optional If set, the reverse homography to back-project to camera objective will be retrieved
Returns ------- M : np.ndarray homography matrix (3x3), used in cv2.warpPerspective """ _src = np.float32(src) _dst = np.float32(dst) if reverse: M = cv2.getPerspectiveTransform(_dst, _src) else: M = cv2.getPerspectiveTransform(_src, _dst) return M
def get_M_3D(src, dst, camera_matrix, dist_coeffs=np.zeros((1, 4)), z=0., reverse=False): """ Retrieve homography matrix for between (6+) 2D src and (6+) 3D dst (x, y, z) points
Parameters ---------- src : list of lists [x, y] with source coordinates, typically cols and rows in image dst : list of lists [x, y, z] with target coordinates after reprojection, can e.g. be in crs [m] camera_matrix : np.ndarray (3x3) Camera intrinsic matrix dist_coeffs : p.ndarray, optional 1xN array with distortion coefficients (N = 4, 5 or 8) z : float, optional Elevation plane (real-world coordinate crs) of projected image reverse : bool, optional If set, the reverse homography to back-project to camera objective will be retrieved
Returns ------- M : np.ndarray homography matrix (3x3), used in cv2.warpPerspective
Notes ----- See rectification workflow OpenCV http://docs.opencv.org/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html Code based on: https://www.openearth.nl/flamingo/_modules/flamingo/rectification/rectification.html
""" _src = np.float32(src) _dst = np.float32(dst) camera_matrix = np.float32(camera_matrix) dist_coeffs = np.float32(dist_coeffs) success, rvec, tvec = cv2.solvePnP(_dst, _src, camera_matrix, dist_coeffs) R = cv2.Rodrigues(rvec)[0] R[:, 2] = R[:, 2] * z R[:, 2] = R[:, 2] + tvec.flatten() if reverse: M = np.dot(camera_matrix, R) else: M = np.linalg.inv(np.dot(camera_matrix, R)) return M / M[-1, -1]
def transform_to_bbox(coords, bbox, resolution): """transforms a set of coordinates defined in crs of bbox, into a set of coordinates in cv2 compatible pixels
Parameters ---------- coords : list of lists [x, y] with coordinates bbox : shapely Polygon Bounding box. The coordinate order is very important and has to be upstream-left, downstream-left, downstream-right, upstream-right, upstream-left resolution : float resolution of target pixels within bbox
Returns ------- colrows : list tuples of columns and rows
""" xs, ys = zip(*coords) transform = _get_transform(bbox, resolution) rows, cols = rasterio.transform.rowcol(transform, xs, ys, op=float) return list(zip(cols, rows))
def _get_transform(bbox, resolution=0.01): """Return a rotated Affine transformation that fits with the bounding box and resolution.
Parameters ---------- bbox : shapely.geometry.Polygon polygon of bounding box. The coordinate order is very important and has to be: (upstream-left, downstream-left, downstream-right, upstream-right, upstream-left) resolution : float, optional resolution of target grid in meters (default: 0.01)
Returns ------- affine : rasterio.transform.Affine """ corners = np.array(bbox.exterior.coords) top_left_x, top_left_y = corners[0] point1 = corners[0] point2 = corners[1] diff = point2 - point1 angle = np.arctan2(diff[1], diff[0]) dx_col, dy_col = np.cos(angle) * resolution, np.sin(angle) * resolution dx_row, dy_row = ( np.cos(angle + 1.5 * np.pi) * resolution, np.sin(angle + 1.5 * np.pi) * resolution, ) return rasterio.transform.Affine( dx_col, dy_col, top_left_x, dx_row, dy_row, top_left_y )
def get_dst_a(h_a=0.0, h_ref=0.0, lens_position=None): """ h_a : float, optional actual water level measured [m], if not set, assumption is that a single video is processed and thus changes in water level are not relevant. (default: None)
Returns ------- Actual locations of control points (in case these are only x, y) given the current set water level and the camera location """ if h_a is None or h_a == h_ref: dst_a = dst else: h_ref = h_ref lens_position = lens_position dst_a = _get_gcps_a( lens_position, h_a, dst, z_0, h_ref, ) return dst_a
def _get_gcps_a(lensPosition, h_a, coords, z_0=0.0, h_ref=0.0): """Get the actual x, y locations of ground control points at the actual water level
Parameters ---------- lensPosition : list of floats x, y, z location of cam in local crs [m] h_a : float actual water level in local level measuring system [m] coords : list of lists gcp coordinates [x, y] in original water level z_0 : float, optional reference zero plain level, i.e. the crs amount of meters of the zero level of staff gauge (default: 0.0) h_ref : float, optional reference water level during taking of gcp coords with ref to staff gauge zero level (default: 0.0)
Returns ------- coords : list rows/cols for use in getPerspectivetransform
""" cam_x, cam_y, cam_z = lensPosition x, y = zip(*coords) z_ref = h_ref + z_0 z_a = z_0 + h_a cam_height_ref = cam_z - z_ref cam_height_a = cam_z - z_a rel_diff = cam_height_a / cam_height_ref _dest_x = list(cam_x + (np.array(x) - cam_x) * rel_diff) _dest_y = list(cam_y + (np.array(y) - cam_y) * rel_diff) dest_out = list(zip(_dest_x, _dest_y)) return dest_out
def get_z_a(h_a=None): """ h_a : float, optional actual water level measured [m], if not set, assumption is that a single video is processed and thus changes in water level are not relevant. (default: None)
Returns ------- Actual locations of control points (in case these are only x, y) given the current set water level and the camera location """ if h_a is None: return z_0 else: return z_0 + (h_a - h_ref)
def shape(): """ Returns rows and columns in projected frames from ``Frames.project``
Returns ------- rows : int Amount of rows in projected frame cols : int Amount of columns in projected frame """ cols, rows = _get_shape( bbox, resolution=resolution, round=1 ) return rows, cols
def _get_shape(bbox, resolution=0.01, round=1): """ defines the number of rows and columns needed in a target raster, to fit a given bounding box.
:param bbox: shapely Polygon, bounding box :param resolution: resolution of target raster :param round: number of pixels to round intended shape to :return: numbers of rows and columns for target raster """ coords = bbox.exterior.coords box_length = LineString(coords[0:2]).length box_width = LineString(coords[1:3]).length cols = int(np.ceil((box_length / resolution) / round)) * round rows = int(np.ceil((box_width / resolution) / round)) * round return cols, rows
if __name__ == '__main__':
src=[ [1421, 1001], [1251, 460], [421, 432], [470, 607] ] dst=[ [642735.8076, 8304292.1190], [642737.5823, 8304295.593], [642732.7864, 8304298.4250], [642732.6705, 8304296.8580] ] corners = [ [292, 817], [50, 166], [1200, 236], [1600, 834] ] resolution = 0.01 h_ref = 0.0 h_a = 0.0 lens_position = None z_0 = 1182.2 gcp_dims = 2 camera_matrix = None dist_coeffs = None
flags = cv2.INTER_AREA bbox, bbox_xy = set_bbox_from_corners(corners, resolution, dst) src_v2 = get_bbox(bbox, dst, camera=True, h_a=0.0).exterior.coords[0:4] dst_xy = bbox_xy dst_v2 = transform_to_bbox(dst_xy, bbox, resolution) M = get_M_2D(src_v2, dst_v2)
video_path = r'ngwerere_20191103.mp4' cap = cv2.VideoCapture(video_path) frame_list = [] while True: ret, frame = cap.read() if ret: frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) frame_list.append(frame) else: break shape = shape() img1 = frame_list[0] img2 = frame_list[1] final_frame1 = cv2.warpPerspective(img1, M, shape, flags=flags) final_frame2 = cv2.warpPerspective(img2, M, shape, flags=flags) cv2.imwrite("transfer_ng1v2_1.png", final_frame1) cv2.imwrite("transfer_ng2v2_2.png", final_frame2)
|