def alt_to_disp(rpc1, rpc2, x, y, alt, H1, H2, A=None): """ Converts an altitude into a disparity. Args: rpc1: an instance of the rpc_model.RPCModel class for the reference image rpc2: an instance of the rpc_model.RPCModel class for the secondary image x, y: coordinates of the point in the reference image alt: altitude above the WGS84 ellipsoid (in meters) of the point H1, H2: rectifying homographies A (optional): pointing correction matrix Returns: the horizontal disparity of the (x, y) point of im1, assuming that the 3-space point associated has altitude alt. The disparity is made horizontal thanks to the two rectifying homographies H1 and H2. """ xx, yy = find_corresponding_point(rpc1, rpc2, x, y, alt)[0:2] p1 = np.vstack([x, y]).T p2 = np.vstack([xx, yy]).T if A is not None: print("rpc_utils.alt_to_disp: applying pointing error correction") # correct coordinates of points in im2, according to A p2 = common.points_apply_homography(np.linalg.inv(A), p2) p1 = common.points_apply_homography(H1, p1) p2 = common.points_apply_homography(H2, p2) # np.testing.assert_allclose(p1[:, 1], p2[:, 1], atol=0.1) disp = p2[:, 0] - p1[:, 0] return disp
def disparity_range_from_matches(matches, H1, H2, w, h): """ Compute the disparity range of a ROI from a list of point matches. The estimation is based on the extrapolation of the affine registration estimated from the matches. The extrapolation is done on the whole region of interest. Args: matches: Nx4 numpy array containing a list of matches, in the full image coordinates frame, before rectification w, h: width and height of the rectangular ROI in the first image. H1, H2: two rectifying homographies, stored as numpy 3x3 matrices Returns: disp_min, disp_max: horizontal disparity range """ # transform the matches according to the homographies p1 = common.points_apply_homography(H1, matches[:, :2]) x1 = p1[:, 0] p2 = common.points_apply_homography(H2, matches[:, 2:]) x2 = p2[:, 0] y2 = p2[:, 1] # compute the final disparity range disp_min = np.floor(np.min(x2 - x1)) disp_max = np.ceil(np.max(x2 - x1)) # add a security margin to the disparity range disp_min *= (1 - np.sign(disp_min) * cfg['disp_range_extra_margin']) disp_max *= (1 + np.sign(disp_max) * cfg['disp_range_extra_margin']) return disp_min, disp_max
def register_horizontally_shear(matches, H1, H2): """ Adjust rectifying homographies with tilt, shear and translation to reduce the disparity range. Args: matches: list of pairs of 2D points, stored as a Nx4 numpy array H1, H2: two homographies, stored as numpy 3x3 matrices Returns: H2: corrected homography H2 The matches are provided in the original images coordinate system. By transforming these coordinates with the provided homographies, we obtain matches whose disparity is only along the x-axis. """ # transform the matches according to the homographies p1 = common.points_apply_homography(H1, matches[:, :2]) x1 = p1[:, 0] y1 = p1[:, 1] p2 = common.points_apply_homography(H2, matches[:, 2:]) x2 = p2[:, 0] y2 = p2[:, 1] if cfg['debug']: print("Residual vertical disparities: max, min, mean. Should be zero") print(np.max(y2 - y1), np.min(y2 - y1), np.mean(y2 - y1)) # we search the (a, b, c) vector that minimises \sum (x1 - (a*x2+b*y2+c))^2 # it is a least squares minimisation problem A = np.vstack((x2, y2, y2 * 0 + 1)).T a, b, c = np.linalg.lstsq(A, x1)[0].flatten() # correct H2 with the estimated tilt, shear and translation return np.dot(np.array([[a, b, c], [0, 1, 0], [0, 0, 1]]), H2)
def register_horizontally_shear(matches, H1, H2): """ Adjust rectifying homographies with a shear to modify the disparity range. Args: matches: list of pairs of 2D points, stored as a Nx4 numpy array H1, H2: two homographies, stored as numpy 3x3 matrices Returns: H2: corrected homography H2 The matches are provided in the original images coordinate system. By transforming these coordinates with the provided homographies, we obtain matches whose disparity is only along the x-axis. """ # transform the matches according to the homographies p1 = common.points_apply_homography(H1, matches[:, :2]) x1 = p1[:, 0] y1 = p1[:, 1] p2 = common.points_apply_homography(H2, matches[:, 2:]) x2 = p2[:, 0] y2 = p2[:, 1] if cfg['debug']: print("Residual vertical disparities: max, min, mean. Should be zero") print(np.max(y2 - y1), np.min(y2 - y1), np.mean(y2 - y1)) # we search the (s, b) vector that minimises \sum (x1 - (x2+s*y2+b))^2 # it is a least squares minimisation problem A = np.vstack((y2, y2*0+1)).T B = x1 - x2 s, b = np.linalg.lstsq(A, B)[0].flatten() # correct H2 with the estimated shear return np.dot(np.array([[1, s, b], [0, 1, 0], [0, 0, 1]]), H2)
def test_affine_transformation(): x = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) # list of transformations to be tested T = np.eye(3) I = np.eye(3) S = np.eye(3) A = np.eye(3) translations = [] isometries = [] similarities = [] affinities = [] for i in range(100): translations.append(T) isometries.append(I) similarities.append(S) affinities.append(A) T[0:2, 2] = np.random.random(2) I = rotation_matrix(2*np.pi * np.random.random_sample()) I[0:2, 2] = np.random.random(2) S = similarity_matrix(2*np.pi * np.random.random_sample(), np.random.random_sample()) S[0:2, 2] = 100 * np.random.random(2) A[0:2, :] = np.random.random((2, 3)) for B in translations + isometries + similarities + affinities: xx = common.points_apply_homography(B, x) E = estimation.affine_transformation(x, xx) assert_array_almost_equal(E, B)
def register_horizontally_translation(matches, H1, H2, flag='center'): """ Adjust rectifying homographies with a translation to modify the disparity range. Args: matches: list of pairs of 2D points, stored as a Nx4 numpy array H1, H2: two homographies, stored as numpy 3x3 matrices flag: option needed to control how to modify the disparity range: 'center': move the barycenter of disparities of matches to zero 'positive': make all the disparities positive 'negative': make all the disparities negative. Required for Hirshmuller stereo (java) Returns: H2: corrected homography H2 The matches are provided in the original images coordinate system. By transforming these coordinates with the provided homographies, we obtain matches whose disparity is only along the x-axis. The second homography H2 is corrected with a horizontal translation to obtain the desired property on the disparity range. """ # transform the matches according to the homographies p1 = common.points_apply_homography(H1, matches[:, :2]) x1 = p1[:, 0] y1 = p1[:, 1] p2 = common.points_apply_homography(H2, matches[:, 2:]) x2 = p2[:, 0] y2 = p2[:, 1] # for debug, print the vertical disparities. Should be zero. if cfg['debug']: print("Residual vertical disparities: max, min, mean. Should be zero") print(np.max(y2 - y1), np.min(y2 - y1), np.mean(y2 - y1)) # compute the disparity offset according to selected option t = 0 if (flag == 'center'): t = np.mean(x2 - x1) if (flag == 'positive'): t = np.min(x2 - x1) if (flag == 'negative'): t = np.max(x2 - x1) # correct H2 with a translation return np.dot(common.matrix_translation(-t, 0), H2)
def cost_function(v, *args): """ Objective function to minimize in order to correct the pointing error. Arguments: v: vector of size 3 or 4, containing the parameters of the euclidean transformation we are looking for. rpc1, rpc2: two instances of the rpc_model.RPCModel class matches: 2D numpy array containing a list of matches. Each line contains one pair of points, ordered as x1 y1 x2 y2. The coordinate system is the one of the big images. alpha: relative weight of the error terms: e + alpha*(h-h0)^2. See paper for more explanations. Returns: The sum of pointing errors and altitude differences, as written in the paper formula (1). """ rpc1, rpc2, matches = args[0], args[1], args[2] if len(args) == 4: alpha = args[3] else: alpha = 0.01 # verify that parameters are in the bounding box if (np.abs(v[0]) > 200 * np.pi or np.abs(v[1]) > 10000 or np.abs(v[2]) > 10000): print('warning: cost_function is going too far') print(v) if (len(v) > 3): if (np.abs(v[3]) > 20000): print('warning: cost_function is going too far') print(v) # compute the altitudes from the matches without correction x1 = matches[:, 0] y1 = matches[:, 1] x2 = matches[:, 2] y2 = matches[:, 3] h0 = rpc_utils.compute_height(rpc1, rpc2, x1, y1, x2, y2)[0] # transform the coordinates of points in the second image according to # matrix A, built from vector v A = euclidean_transform_matrix(v) p2 = common.points_apply_homography(A, matches[:, 2:4]) x2 = p2[:, 0] y2 = p2[:, 1] # compute the cost h, e = rpc_utils.compute_height(rpc1, rpc2, x1, y1, x2, y2) cost = np.sum((h - h0)**2) cost *= alpha cost += np.sum(e) #print cost return cost
def rectification_homographies(matches, x, y, w, h, hmargin=0, vmargin=0): """ Computes rectifying homographies from point matches for a given ROI. The affine fundamental matrix F is estimated with the gold-standard algorithm, then two rectifying similarities (rotation, zoom, translation) are computed directly from F. Args: matches: numpy array of shape (n, 4) containing a list of 2D point correspondences between the two images. x, y, w, h: four integers defining the rectangular ROI in the first image. (x, y) is the top-left corner, and (w, h) are the dimensions of the rectangle. {h,v}margin: translations added to the rectifying similarities to extend the horizontal and vertical footprint of the rectified images Returns: S1, S2, F: three numpy arrays of shape (3, 3) representing the two rectifying similarities to be applied to the two images and the corresponding affine fundamental matrix. """ # estimate the affine fundamental matrix with the Gold standard algorithm F = estimation.affine_fundamental_matrix(matches) # compute rectifying similarities S1, S2 = estimation.rectifying_similarities_from_affine_fundamental_matrix( F, cfg['debug']) if cfg['debug']: y1 = common.points_apply_homography(S1, matches[:, :2])[:, 1] y2 = common.points_apply_homography(S2, matches[:, 2:])[:, 1] err = np.abs(y1 - y2) print("max, min, mean rectification error on point matches: ", end=' ') print(np.max(err), np.min(err), np.mean(err)) # pull back top-left corner of the ROI to the origin (plus margin) pts = common.points_apply_homography( S1, [[x, y], [x + w, y], [x + w, y + h], [x, y + h]]) x0, y0 = common.bounding_box2D(pts)[:2] T = common.matrix_translation(-x0 + hmargin, -y0 + vmargin) return np.dot(T, S1), np.dot(T, S2), F
def disparity_range_from_matches(matches, H1, H2, w, h): """ Compute the disparity range of a ROI from a list of point matches. The estimation is based on the extrapolation of the affine registration estimated from the matches. The extrapolation is done on the whole region of interest. Args: matches: Nx4 numpy array containing a list of matches, in the full image coordinates frame, before rectification w, h: width and height of the rectangular ROI in the first image. H1, H2: two rectifying homographies, stored as numpy 3x3 matrices Returns: disp_min, disp_max: horizontal disparity range """ # transform the matches according to the homographies p1 = common.points_apply_homography(H1, matches[:, :2]) x1 = p1[:, 0] p2 = common.points_apply_homography(H2, matches[:, 2:]) x2 = p2[:, 0] y2 = p2[:, 1] # estimate an affine transformation (tilt, shear and bias) mapping p1 on p2 t, s, b = np.linalg.lstsq(np.vstack((x2, y2, y2 * 0 + 1)).T, x1)[0][:3] # compute the disparities for the affine model. The extrema are obtained at # the ROI corners xx = np.array([0, w, 0, w]) yy = np.array([0, 0, h, h]) disp_affine_model = (xx * t + yy * s + b) - xx # compute the final disparity range disp_min = np.floor(min(np.min(disp_affine_model), np.min(x2 - x1))) disp_max = np.ceil(max(np.max(disp_affine_model), np.max(x2 - x1))) # add a security margin to the disparity range disp_min *= (1 - np.sign(disp_min) * cfg['disp_range_extra_margin']) disp_max *= (1 + np.sign(disp_max) * cfg['disp_range_extra_margin']) return disp_min, disp_max
def evaluation_from_estimated_F(im1, im2, rpc1, rpc2, x, y, w, h, A=None, matches=None): """ Measures the pointing error on a Pleiades' pair of images, affine approx. Args: im1, im2: paths to the two Pleiades images (usually jp2 or tif) rpc1, rpc2: two instances of the rpc_model.RPCModel class x, y, w, h: four integers defining the rectangular ROI in the first image. (x, y) is the top-left corner, and (w, h) are the dimensions of the rectangle. A (optional): 3x3 numpy array containing the pointing error correction for im2. matches (optional): Nx4 numpy array containing a list of matches to use to compute the pointing error Returns: the mean pointing error, in the direction orthogonal to the epipolar lines. This error is measured in pixels, and computed from an approximated fundamental matrix. """ if not matches: matches = sift.matches_on_rpc_roi(im1, im2, rpc1, rpc2, x, y, w, h) p1 = matches[:, 0:2] p2 = matches[:, 2:4] print('%d sift matches' % len(matches)) # apply pointing correction matrix, if available if A is not None: p2 = common.points_apply_homography(A, p2) # estimate the fundamental matrix between the two views rpc_matches = rpc_utils.matches_from_rpc(rpc1, rpc2, x, y, w, h, 5) F = estimation.affine_fundamental_matrix(rpc_matches) # compute the mean displacement from epipolar lines d_sum = 0 for i in range(len(p1)): x = np.array([p1[i, 0], p1[i, 1], 1]) xx = np.array([p2[i, 0], p2[i, 1], 1]) ll = F.dot(x) #d = np.sign(xx.dot(ll)) * evaluation.distance_point_to_line(xx, ll) d = evaluation.distance_point_to_line(xx, ll) d_sum += d return d_sum / len(p1)
def rectification_homographies(matches, x, y, w, h, hmargin=0, vmargin=0): """ Computes rectifying homographies from point matches for a given ROI. The affine fundamental matrix F is estimated with the gold-standard algorithm, then two rectifying similarities (rotation, zoom, translation) are computed directly from F. Args: matches: numpy array of shape (n, 4) containing a list of 2D point correspondences between the two images. x, y, w, h: four integers defining the rectangular ROI in the first image. (x, y) is the top-left corner, and (w, h) are the dimensions of the rectangle. {h,v}margin: translations added to the rectifying similarities to extend the horizontal and vertical footprint of the rectified images Returns: S1, S2, F: three numpy arrays of shape (3, 3) representing the two rectifying similarities to be applied to the two images and the corresponding affine fundamental matrix. """ # estimate the affine fundamental matrix with the Gold standard algorithm F = estimation.affine_fundamental_matrix(matches) # compute rectifying similarities S1, S2 = estimation.rectifying_similarities_from_affine_fundamental_matrix(F, cfg['debug']) if cfg['debug']: y1 = common.points_apply_homography(S1, matches[:, :2])[:, 1] y2 = common.points_apply_homography(S2, matches[:, 2:])[:, 1] err = np.abs(y1 - y2) print("max, min, mean rectification error on point matches: ", end=' ') print(np.max(err), np.min(err), np.mean(err)) # pull back top-left corner of the ROI to the origin (plus margin) pts = common.points_apply_homography(S1, [[x, y], [x+w, y], [x+w, y+h], [x, y+h]]) x0, y0 = common.bounding_box2D(pts)[:2] T = common.matrix_translation(-x0 + hmargin, -y0 + vmargin) return np.dot(T, S1), np.dot(T, S2), F
def evaluation_iterative(im1, im2, rpc1, rpc2, x, y, w, h, A=None, matches=None): """ Measures the maximal pointing error on a Pleiades' pair of images. Args: im1, im2: paths to the two Pleiades images (usually jp2 or tif) rpc1, rpc2: two instances of the rpc_model.RPCModel class x, y, w, h: four integers defining the rectangular ROI in the first image. (x, y) is the top-left corner, and (w, h) are the dimensions of the rectangle. A (optional): 3x3 numpy array containing the pointing error correction for im2. matches (optional): Nx4 numpy array containing a list of matches to use to compute the pointing error Returns: the mean pointing error, in the direction orthogonal to the epipolar lines. This error is measured in pixels. """ if not matches: matches = sift.matches_on_rpc_roi(im1, im2, rpc1, rpc2, x, y, w, h) p1 = matches[:, 0:2] p2 = matches[:, 2:4] print('%d sift matches' % len(matches)) # apply pointing correction matrix, if available if A is not None: p2 = common.points_apply_homography(A, p2) # compute the pointing error for each match x1 = p1[:, 0] y1 = p1[:, 1] x2 = p2[:, 0] y2 = p2[:, 1] e = rpc_utils.compute_height(rpc1, rpc2, x1, y1, x2, y2)[1] # matches = matches[e < 0.1, :] # visualisation.plot_matches_pleiades(im1, im2, matches) print("max, mean, min pointing error, from %d points:" % (len(matches))) print(np.max(e), np.mean(e), np.min(e)) # return the mean error return np.mean(np.abs(e))
def plot_pointing_error_tile(im1, im2, rpc1, rpc2, x, y, w, h, matches_sift=None, f=100, out_files_pattern=None): """ Args: im1, im2: path to full images rpc1, rcp2: path to associated rpc xml files x, y, w, h: four integers defining the rectangular tile in the reference image. (x, y) is the top-left corner, and (w, h) are the dimensions of the tile. f (optional, default is 100): exageration factor for the error vectors out_files_pattern (optional, default is None): pattern used to name the two output files (plots of the pointing error) Returns: nothing, but opens a display """ # read rpcs r1 = rpc_model.RPCModel(rpc1) r2 = rpc_model.RPCModel(rpc2) # compute sift matches if not matches_sift: matches_sift = sift.matches_on_rpc_roi(im1, im2, r1, r2, x, y, w, h) # compute rpc matches matches_rpc = rpc_utils.matches_from_rpc(r1, r2, x, y, w, h, 5) # estimate affine fundamental matrix F = estimation.affine_fundamental_matrix(matches_rpc) # compute error vectors e = s2plib.pointing_accuracy.error_vectors(matches_sift, F, 'ref') A = s2plib.pointing_accuracy.local_translation(r1,r2, x,y,w,h, matches_sift) p = matches_sift[:, 0:2] q = matches_sift[:, 2:4] qq = common.points_apply_homography(A, q) ee = s2plib.pointing_accuracy.error_vectors(np.hstack((p, qq)), F, 'ref') print(s2plib.pointing_accuracy.evaluation_from_estimated_F(im1, im2, r1, r2, x, y, w, h, None, matches_sift)) print(s2plib.pointing_accuracy.evaluation_from_estimated_F(im1, im2, r1, r2, x, y, w, h, A, matches_sift)) # plot the vectors: they go from the point x to the line (F.T)x' plot_vectors(p, -e, x, y, w, h, f, out_file='%s_before.png' % out_files_pattern) plot_vectors(p, -ee, x, y, w, h, f, out_file='%s_after.png' % out_files_pattern)
def cost_function_linear(v, rpc1, rpc2, matches): """ Objective function to minimize in order to correct the pointing error. Arguments: v: vector of size 4, containing the 4 parameters of the euclidean transformation we are looking for. rpc1, rpc2: two instances of the rpc_model.RPCModel class matches: 2D numpy array containing a list of matches. Each line contains one pair of points, ordered as x1 y1 x2 y2. The coordinate system is the one of the big images. alpha: relative weight of the error terms: e + alpha*(h-h0)^2. See paper for more explanations. Returns: The sum of pointing errors and altitude differences, as written in the paper formula (1). """ print_params(v) # verify that parameters are in the bounding box if (np.abs(v[0]) > 200 * np.pi or np.abs(v[1]) > 10000 or np.abs(v[2]) > 10000 or np.abs(v[3]) > 20000): print('warning: cost_function is going too far') print(v) x, y, w, h = common.bounding_box2D(matches[:, 0:2]) matches_rpc = rpc_utils.matches_from_rpc(rpc1, rpc2, x, y, w, h, 5) F = estimation.fundamental_matrix(matches_rpc) # transform the coordinates of points in the second image according to # matrix A, built from vector v A = euclidean_transform_matrix(v) p2 = common.points_apply_homography(A, matches[:, 2:4]) return evaluation.fundamental_matrix_L1(F, np.hstack([matches[:, 0:2], p2]))
def disparity_to_ply(tile): """ Compute a point cloud from the disparity map of a pair of image tiles. Args: tile: dictionary containing the information needed to process a tile. """ out_dir = os.path.join(tile['dir']) ply_file = os.path.join(out_dir, 'cloud.ply') plyextrema = os.path.join(out_dir, 'plyextrema.txt') x, y, w, h = tile['coordinates'] rpc1 = cfg['images'][0]['rpc'] rpc2 = cfg['images'][1]['rpc'] if os.path.exists(os.path.join(out_dir, 'stderr.log')): print('triangulation: stderr.log exists') print('pair_1 not processed on tile {} {}'.format(x, y)) return if cfg['skip_existing'] and os.path.isfile(ply_file): print('triangulation done on tile {} {}'.format(x, y)) return print('triangulating tile {} {}...'.format(x, y)) # This function is only called when there is a single pair (pair_1) H_ref = os.path.join(out_dir, 'pair_1', 'H_ref.txt') H_sec = os.path.join(out_dir, 'pair_1', 'H_sec.txt') pointing = os.path.join(cfg['out_dir'], 'global_pointing_pair_1.txt') disp = os.path.join(out_dir, 'pair_1', 'rectified_disp.tif') mask_rect = os.path.join(out_dir, 'pair_1', 'rectified_mask.png') mask_orig = os.path.join(out_dir, 'cloud_water_image_domain_mask.png') # prepare the image needed to colorize point cloud colors = os.path.join(out_dir, 'rectified_ref.png') if cfg['images'][0]['clr']: hom = np.loadtxt(H_ref) roi = [[x, y], [x + w, y], [x + w, y + h], [x, y + h]] ww, hh = common.bounding_box2D(common.points_apply_homography( hom, roi))[2:] tmp = common.tmpfile('.tif') common.image_apply_homography(tmp, cfg['images'][0]['clr'], hom, ww + 2 * cfg['horizontal_margin'], hh + 2 * cfg['vertical_margin']) common.image_qauto(tmp, colors) else: common.image_qauto( os.path.join(out_dir, 'pair_1', 'rectified_ref.tif'), colors) # compute the point cloud triangulation.disp_map_to_point_cloud(ply_file, disp, mask_rect, rpc1, rpc2, H_ref, H_sec, pointing, colors, utm_zone=cfg['utm_zone'], llbbx=tuple(cfg['ll_bbx']), xybbx=(x, x + w, y, y + h), xymsk=mask_orig) # compute the point cloud extrema (xmin, xmax, xmin, ymax) common.run("plyextrema %s %s" % (ply_file, plyextrema)) if cfg['clean_intermediate']: common.remove(H_ref) common.remove(H_sec) common.remove(disp) common.remove(mask_rect) common.remove(mask_orig) common.remove(colors) common.remove(os.path.join(out_dir, 'pair_1', 'rectified_ref.tif'))
def local_translation_rotation(r1, r2, x, y, w, h, m): """ Estimates the optimal translation to minimise the relative pointing error on a given tile. Args: r1, r2: two instances of the rpc_model.RPCModel class x, y, w, h: region of interest in the reference image (r1) m: Nx4 numpy array containing a list of matches, one per line. Each match is given by (p1, p2, q1, q2) where (p1, p2) is a point of the reference view and (q1, q2) is the corresponding point in the secondary view. Returns: 3x3 numpy array containing the homogeneous representation of the optimal planar translation, to be applied to the secondary image in order to correct the pointing error. """ # estimate the affine fundamental matrix between the two views n = cfg['n_gcp_per_axis'] rpc_matches = rpc_utils.matches_from_rpc(r1, r2, x, y, w, h, n) F = estimation.affine_fundamental_matrix(rpc_matches) # Compute rectification homographies # S1, S2 = estimation.rectifying_similarities_from_affine_fundamental_matrix(F, cfg['debug']) hmargin = cfg['horizontal_margin'] vmargin = cfg['vertical_margin'] S1, S2, F_rect = rectification.rectification_homographies(rpc_matches, x, y, w, h, hmargin, vmargin) N = len(m) # Apply rectification on image 1 S1p1 = common.points_apply_homography(S1, m[:,0:2]) S1p1 = np.column_stack((S1p1,np.ones((N,1)))) print("Points 1 rectied") print(S1p1[:10]) # Apply rectification on image 2 S2p2 = common.points_apply_homography(S1, m[:,2:4]) S2p2 = np.column_stack((S2p2, np.ones((N,1)))) print("Points 2 rectied") print(S2p2[:10]) # Compute F in the rectified space rect_matches = np.column_stack((S1p1[:,0:2], S2p2[:, 0:2])) F_rect2 = estimation.affine_fundamental_matrix(rect_matches) # Compute epipolar lines FS1p1 = np.dot(F_rect2, S1p1.T).T print(FS1p1[:10]) # Normalize epipolar lines c1 = -FS1p1[:, 1] FS1p1_norm = FS1p1/c1[:, np.newaxis] print(FS1p1_norm[:10]) # Variable of optimization problem A_ = np.ones((N,1)) # A_ = np.column_stack((S2p2[:,0].reshape(N, 1),np.ones((N,1)))) # b_ = S2p2[:, 1] - FS1p1_norm[:, 2] b_ = S2p2[:, 1] - S1p1[:, 1] t_med = np.median(b_) print(t_med) t_med2 = np.sort(b_)[int(N/2)] print("t_med2", t_med2) # min ||Ax + b||^2 => x = - (A^T A )^-1 A^T b # X_ = - np.dot(np.linalg.inv(np.dot(A_.T, A_)), np.dot(A_.T, b_)) # [theta, t] = X_ # print(t, theta) # t = X_[0] # Compute epipolar lines witout recitifcation p1 = np.column_stack((m[:,0:2],np.ones((N,1)))) Fp1 = np.dot(F, p1.T).T # Compute normal vector to epipolar liness ab = np.array([Fp1[0][0], Fp1[0][1]]) ab = ab/np.linalg.norm(ab) tx = t_med*ab[0] ty = t_med*ab[1] print(tx, ty) # Get translation in not rectified image T = common.matrix_translation(0, -t_med) T = np.linalg.inv(S2).dot(T).dot(S2) # T = np.dot(S2_inv, T) print(T) theta = 0 cos_theta = np.cos(theta) sin_theta = np.sin(theta) A = np.array([[cos_theta, -sin_theta, tx], [sin_theta, cos_theta, ty], [0, 0, 1]]) return A, F
def local_translation_rectified(r1, r2, x, y, w, h, m): """ Estimates the optimal translation to minimise the relative pointing error on a given tile. Args: r1, r2: two instances of the rpc_model.RPCModel class x, y, w, h: region of interest in the reference image (r1) m: Nx4 numpy array containing a list of matches, one per line. Each match is given by (p1, p2, q1, q2) where (p1, p2) is a point of the reference view and (q1, q2) is the corresponding point in the secondary view. Returns: 3x3 numpy array containing the homogeneous representation of the optimal planar translation, to be applied to the secondary image in order to correct the pointing error. """ # estimate the affine fundamental matrix between the two views n = cfg['n_gcp_per_axis'] rpc_matches = rpc_utils.matches_from_rpc(r1, r2, x, y, w, h, n) F = estimation.affine_fundamental_matrix(rpc_matches) # Apply rectification on image 1 S1p1 = common.points_apply_homography(S1, m[:,0:2]) S1p1 = np.column_stack((S1p1,np.ones((N,1)))) print("Points 1 rectied") print(S1p1[:10]) # Apply rectification on image 2 S2p2 = common.points_apply_homography(S1, m[:,2:4]) S2p2 = np.column_stack((S2p2, np.ones((N,1)))) print("Points 2 rectied") print(S2p2[:10]) # Compute F in the rectified space rect_matches = np.column_stack((S1p1[:,0:2], S2p2[:, 0:2])) F_rect2 = estimation.affine_fundamental_matrix(rect_matches) # Compute epipolar lines FS1p1 = np.dot(F_rect2, S1p1.T).T print(FS1p1[:10]) # Normalize epipolar lines c1 = -FS1p1[:, 1] FS1p1_norm = FS1p1/c1[:, np.newaxis] print(FS1p1_norm[:10]) b_ = np.abs(S2p2[:, 1] - S1p1[:, 1]) t_med = np.median(b_) print(t_med) t_med2 = np.sort(b_)[int(N/2)] print("t_med2", t_med2) # Compute epipolar lines witout recitifcation p1 = np.column_stack((m[:,0:2],np.ones((N,1)))) Fp1 = np.dot(F, p1.T).T # Compute normal vector to epipolar liness ab = np.array([Fp1[0][0], Fp1[0][1]]) ab = ab/np.linalg.norm(ab) tx = t_med*ab[0] ty = t_med*ab[1] print(tx, ty) # Get translation in not rectified image T = common.matrix_translation(0, -t_med) T = np.linalg.inv(S2).dot(T).dot(S2) # T = np.dot(S2_inv, T) print(T) # the correction to be applied to the second view is the opposite A = np.array([[1, 0, -out_x], [0, 1, -out_y], [0, 0, 1]]) return A, F
def rectify_pair(im1, im2, rpc1, rpc2, x, y, w, h, out1, out2, A=None, sift_matches=None, method='rpc', hmargin=0, vmargin=0): """ Rectify a ROI in a pair of images. Args: im1, im2: paths to two image files rpc1, rpc2: paths to the two xml files containing RPC data x, y, w, h: four integers defining the rectangular ROI in the first image. (x, y) is the top-left corner, and (w, h) are the dimensions of the rectangle. out1, out2: paths to the output rectified crops A (optional): 3x3 numpy array containing the pointing error correction for im2. This matrix is usually estimated with the pointing_accuracy module. sift_matches (optional): Nx4 numpy array containing a list of sift matches, in the full image coordinates frame method (default: 'rpc'): option to decide wether to use rpc of sift matches for the fundamental matrix estimation. {h,v}margin (optional): horizontal and vertical margins added on the sides of the rectified images Returns: H1, H2: Two 3x3 matrices representing the rectifying homographies that have been applied to the two original (large) images. disp_min, disp_max: horizontal disparity range """ # read RPC data rpc1 = rpc_model.RPCModel(rpc1) rpc2 = rpc_model.RPCModel(rpc2) # compute real or virtual matches if method == 'rpc': # find virtual matches from RPC camera models matches = rpc_utils.matches_from_rpc(rpc1, rpc2, x, y, w, h, cfg['n_gcp_per_axis']) # correct second image coordinates with the pointing correction matrix if A is not None: matches[:, 2:] = common.points_apply_homography( np.linalg.inv(A), matches[:, 2:]) else: matches = sift_matches # compute rectifying homographies H1, H2, F = rectification_homographies(matches, x, y, w, h, hmargin, vmargin) if cfg['register_with_shear']: # compose H2 with a horizontal shear to reduce the disparity range a = np.mean(rpc_utils.altitude_range(rpc1, x, y, w, h)) lon, lat, alt = rpc_utils.ground_control_points( rpc1, x, y, w, h, a, a, 4) x1, y1 = rpc1.inverse_estimate(lon, lat, alt)[:2] x2, y2 = rpc2.inverse_estimate(lon, lat, alt)[:2] m = np.vstack([x1, y1, x2, y2]).T m = np.vstack({tuple(row) for row in m}) # remove duplicates due to no alt range H2 = register_horizontally_shear(m, H1, H2) # compose H2 with a horizontal translation to center disp range around 0 if sift_matches is not None: sift_matches = filter_matches_epipolar_constraint( F, sift_matches, cfg['epipolar_thresh']) if len(sift_matches) < 10: print('WARNING: no registration with less than 10 matches') else: H2 = register_horizontally_translation(sift_matches, H1, H2) # compute disparity range if cfg['debug']: out_dir = os.path.dirname(out1) np.savetxt(os.path.join(out_dir, 'sift_matches_disp.txt'), sift_matches, fmt='%9.3f') visualisation.plot_matches( im1, im2, rpc1, rpc2, sift_matches, x, y, w, h, os.path.join(out_dir, 'sift_matches_disp.png')) disp_m, disp_M = disparity_range(rpc1, rpc2, x, y, w, h, H1, H2, sift_matches, A) # compute rectifying homographies for non-epipolar mode (rectify the secondary tile only) if block_matching.rectify_secondary_tile_only(cfg['matching_algorithm']): H1_inv = np.linalg.inv(H1) H1 = np.eye( 3 ) # H1 is replaced by 2-D array with ones on the diagonal and zeros elsewhere H2 = np.dot(H1_inv, H2) T = common.matrix_translation(-x + hmargin, -y + vmargin) H1 = np.dot(T, H1) H2 = np.dot(T, H2) # compute output images size roi = [[x, y], [x + w, y], [x + w, y + h], [x, y + h]] pts1 = common.points_apply_homography(H1, roi) x0, y0, w0, h0 = common.bounding_box2D(pts1) # check that the first homography maps the ROI in the positive quadrant np.testing.assert_allclose(np.round([x0, y0]), [hmargin, vmargin], atol=.01) # apply homographies and do the crops common.image_apply_homography(out1, im1, H1, w0 + 2 * hmargin, h0 + 2 * vmargin) common.image_apply_homography(out2, im2, H2, w0 + 2 * hmargin, h0 + 2 * vmargin) if block_matching.rectify_secondary_tile_only(cfg['matching_algorithm']): pts_in = [[0, 0], [disp_m, 0], [disp_M, 0]] pts_out = common.points_apply_homography(H1_inv, pts_in) disp_m = pts_out[1, :] - pts_out[0, :] disp_M = pts_out[2, :] - pts_out[0, :] return H1, H2, disp_m, disp_M
def rectify_pair(im1, im2, rpc1, rpc2, x, y, w, h, out1, out2, A=None, sift_matches=None, method='rpc', hmargin=0, vmargin=0): """ Rectify a ROI in a pair of images. Args: im1, im2: paths to two image files rpc1, rpc2: paths to the two xml files containing RPC data x, y, w, h: four integers defining the rectangular ROI in the first image. (x, y) is the top-left corner, and (w, h) are the dimensions of the rectangle. out1, out2: paths to the output rectified crops A (optional): 3x3 numpy array containing the pointing error correction for im2. This matrix is usually estimated with the pointing_accuracy module. sift_matches (optional): Nx4 numpy array containing a list of sift matches, in the full image coordinates frame method (default: 'rpc'): option to decide wether to use rpc of sift matches for the fundamental matrix estimation. {h,v}margin (optional): horizontal and vertical margins added on the sides of the rectified images This function uses the parameter subsampling_factor from the config module. If the factor z > 1 then the output images will be subsampled by a factor z. The output matrices H1, H2, and the ranges are also updated accordingly: Hi = Z * Hi with Z = diag(1/z, 1/z, 1) and disp_min = disp_min / z (resp _max) Returns: H1, H2: Two 3x3 matrices representing the rectifying homographies that have been applied to the two original (large) images. disp_min, disp_max: horizontal disparity range """ # read RPC data rpc1 = rpc_model.RPCModel(rpc1) rpc2 = rpc_model.RPCModel(rpc2) # compute real or virtual matches if method == 'rpc': # find virtual matches from RPC camera models matches = rpc_utils.matches_from_rpc(rpc1, rpc2, x, y, w, h, cfg['n_gcp_per_axis']) # correct second image coordinates with the pointing correction matrix if A is not None: matches[:, 2:] = common.points_apply_homography( np.linalg.inv(A), matches[:, 2:]) else: matches = sift_matches # compute rectifying homographies H1, H2, F = rectification_homographies(matches, x, y, w, h, hmargin, vmargin) if cfg['register_with_shear']: # compose H2 with a horizontal shear to reduce the disparity range a = np.mean(rpc_utils.altitude_range(rpc1, x, y, w, h)) lon, lat, alt = rpc_utils.ground_control_points( rpc1, x, y, w, h, a, a, 4) x1, y1 = rpc1.inverse_estimate(lon, lat, alt)[:2] x2, y2 = rpc2.inverse_estimate(lon, lat, alt)[:2] m = np.vstack([x1, y1, x2, y2]).T m = np.vstack({tuple(row) for row in m}) # remove duplicates due to no alt range H2 = register_horizontally_shear(m, H1, H2) # compose H2 with a horizontal translation to center disp range around 0 if sift_matches is not None: sift_matches = filter_matches_epipolar_constraint( F, sift_matches, cfg['epipolar_thresh']) if len(sift_matches) < 10: print('WARNING: no registration with less than 10 matches') else: H2 = register_horizontally_translation(sift_matches, H1, H2) # compute disparity range if cfg['debug']: out_dir = os.path.dirname(out1) np.savetxt(os.path.join(out_dir, 'sift_matches_disp.txt'), sift_matches, fmt='%9.3f') visualisation.plot_matches( im1, im2, rpc1, rpc2, sift_matches, x, y, w, h, os.path.join(out_dir, 'sift_matches_disp.png')) disp_m, disp_M = disparity_range(rpc1, rpc2, x, y, w, h, H1, H2, sift_matches, A) # impose a minimal disparity range (TODO this is valid only with the # 'center' flag for register_horizontally_translation) disp_m = min(-3, disp_m) disp_M = max(3, disp_M) # if subsampling_factor'] the homographies are altered to reflect the zoom z = cfg['subsampling_factor'] if z != 1: Z = np.diag((1 / z, 1 / z, 1)) H1 = np.dot(Z, H1) H2 = np.dot(Z, H2) disp_m = np.floor(disp_m / z) disp_M = np.ceil(disp_M / z) hmargin = int(np.floor(hmargin / z)) vmargin = int(np.floor(vmargin / z)) # compute output images size roi = [[x, y], [x + w, y], [x + w, y + h], [x, y + h]] pts1 = common.points_apply_homography(H1, roi) x0, y0, w0, h0 = common.bounding_box2D(pts1) # check that the first homography maps the ROI in the positive quadrant np.testing.assert_allclose(np.round([x0, y0]), [hmargin, vmargin], atol=.01) # apply homographies and do the crops common.image_apply_homography(out1, im1, H1, w0 + 2 * hmargin, h0 + 2 * vmargin) common.image_apply_homography(out2, im2, H2, w0 + 2 * hmargin, h0 + 2 * vmargin) return H1, H2, disp_m, disp_M
def disparity_to_ply(tile): """ Compute a point cloud from the disparity map of a pair of image tiles. Args: tile: dictionary containing the information needed to process a tile. """ out_dir = os.path.join(tile['dir']) ply_file = os.path.join(out_dir, 'cloud.ply') plyextrema = os.path.join(out_dir, 'plyextrema.txt') x, y, w, h = tile['coordinates'] rpc1 = cfg['images'][0]['rpc'] rpc2 = cfg['images'][1]['rpc'] if os.path.exists(os.path.join(out_dir, 'stderr.log')): print('triangulation: stderr.log exists') print('pair_1 not processed on tile {} {}'.format(x, y)) return if cfg['skip_existing'] and os.path.isfile(ply_file): print('triangulation done on tile {} {}'.format(x, y)) return print('triangulating tile {} {}...'.format(x, y)) # This function is only called when there is a single pair (pair_1) H_ref = os.path.join(out_dir, 'pair_1', 'H_ref.txt') H_sec = os.path.join(out_dir, 'pair_1', 'H_sec.txt') pointing = os.path.join(cfg['out_dir'], 'global_pointing_pair_1.txt') disp = os.path.join(out_dir, 'pair_1', 'rectified_disp.tif') mask_rect = os.path.join(out_dir, 'pair_1', 'rectified_mask.png') mask_orig = os.path.join(out_dir, 'cloud_water_image_domain_mask.png') # prepare the image needed to colorize point cloud colors = os.path.join(out_dir, 'rectified_ref.png') if cfg['images'][0]['clr']: hom = np.loadtxt(H_ref) roi = [[x, y], [x+w, y], [x+w, y+h], [x, y+h]] ww, hh = common.bounding_box2D(common.points_apply_homography(hom, roi))[2:] tmp = common.tmpfile('.tif') common.image_apply_homography(tmp, cfg['images'][0]['clr'], hom, ww + 2*cfg['horizontal_margin'], hh + 2*cfg['vertical_margin']) common.image_qauto(tmp, colors) else: common.image_qauto(os.path.join(out_dir, 'pair_1', 'rectified_ref.tif'), colors) # compute the point cloud triangulation.disp_map_to_point_cloud(ply_file, disp, mask_rect, rpc1, rpc2, H_ref, H_sec, pointing, colors, utm_zone=cfg['utm_zone'], llbbx=tuple(cfg['ll_bbx']), xybbx=(x, x+w, y, y+h), xymsk=mask_orig) # compute the point cloud extrema (xmin, xmax, xmin, ymax) common.run("plyextrema %s %s" % (ply_file, plyextrema)) if cfg['clean_intermediate']: common.remove(H_ref) common.remove(H_sec) common.remove(disp) common.remove(mask_rect) common.remove(mask_orig) common.remove(colors) common.remove(os.path.join(out_dir, 'pair_1', 'rectified_ref.tif'))
def rectify_pair(im1, im2, rpc1, rpc2, x, y, w, h, out1, out2, A=None, sift_matches=None, method='rpc', hmargin=0, vmargin=0): """ Rectify a ROI in a pair of images. Args: im1, im2: paths to two image files rpc1, rpc2: paths to the two xml files containing RPC data x, y, w, h: four integers defining the rectangular ROI in the first image. (x, y) is the top-left corner, and (w, h) are the dimensions of the rectangle. out1, out2: paths to the output rectified crops A (optional): 3x3 numpy array containing the pointing error correction for im2. This matrix is usually estimated with the pointing_accuracy module. sift_matches (optional): Nx4 numpy array containing a list of sift matches, in the full image coordinates frame method (default: 'rpc'): option to decide wether to use rpc of sift matches for the fundamental matrix estimation. {h,v}margin (optional): horizontal and vertical margins added on the sides of the rectified images This function uses the parameter subsampling_factor from the config module. If the factor z > 1 then the output images will be subsampled by a factor z. The output matrices H1, H2, and the ranges are also updated accordingly: Hi = Z * Hi with Z = diag(1/z, 1/z, 1) and disp_min = disp_min / z (resp _max) Returns: H1, H2: Two 3x3 matrices representing the rectifying homographies that have been applied to the two original (large) images. disp_min, disp_max: horizontal disparity range """ # read RPC data rpc1 = rpc_model.RPCModel(rpc1) rpc2 = rpc_model.RPCModel(rpc2) # compute real or virtual matches if method == 'rpc': # find virtual matches from RPC camera models matches = rpc_utils.matches_from_rpc(rpc1, rpc2, x, y, w, h, cfg['n_gcp_per_axis']) # correct second image coordinates with the pointing correction matrix if A is not None: matches[:, 2:] = common.points_apply_homography(np.linalg.inv(A), matches[:, 2:]) else: matches = sift_matches # compute rectifying homographies H1, H2, F = rectification_homographies(matches, x, y, w, h, hmargin, vmargin) if cfg['register_with_shear']: # compose H2 with a horizontal shear to reduce the disparity range a = np.mean(rpc_utils.altitude_range(rpc1, x, y, w, h)) lon, lat, alt = rpc_utils.ground_control_points(rpc1, x, y, w, h, a, a, 4) x1, y1 = rpc1.inverse_estimate(lon, lat, alt)[:2] x2, y2 = rpc2.inverse_estimate(lon, lat, alt)[:2] m = np.vstack([x1, y1, x2, y2]).T m = np.vstack({tuple(row) for row in m}) # remove duplicates due to no alt range H2 = register_horizontally_shear(m, H1, H2) # compose H2 with a horizontal translation to center disp range around 0 if sift_matches is not None: sift_matches = filter_matches_epipolar_constraint(F, sift_matches, cfg['epipolar_thresh']) if len(sift_matches) < 10: print('WARNING: no registration with less than 10 matches') else: H2 = register_horizontally_translation(sift_matches, H1, H2) # compute disparity range if cfg['debug']: out_dir = os.path.dirname(out1) np.savetxt(os.path.join(out_dir, 'sift_matches_disp.txt'), sift_matches, fmt='%9.3f') visualisation.plot_matches(im1, im2, rpc1, rpc2, sift_matches, x, y, w, h, os.path.join(out_dir, 'sift_matches_disp.png')) disp_m, disp_M = disparity_range(rpc1, rpc2, x, y, w, h, H1, H2, sift_matches, A) # impose a minimal disparity range (TODO this is valid only with the # 'center' flag for register_horizontally_translation) disp_m = min(-3, disp_m) disp_M = max(3, disp_M) # compute rectifying homographies for non-epipolar mode (rectify the secondary tile only) if block_matching.rectify_secondary_tile_only(cfg['matching_algorithm']): H1_inv = np.linalg.inv(H1) H1 = np.eye(3) # H1 is replaced by 2-D array with ones on the diagonal and zeros elsewhere H2 = np.dot(H1_inv,H2) T = common.matrix_translation(-x + hmargin, -y + vmargin) H1 = np.dot(T, H1) H2 = np.dot(T, H2) # if subsampling_factor'] the homographies are altered to reflect the zoom z = cfg['subsampling_factor'] if z != 1: Z = np.diag((1/z, 1/z, 1)) H1 = np.dot(Z, H1) H2 = np.dot(Z, H2) disp_m = np.floor(disp_m / z) disp_M = np.ceil(disp_M / z) hmargin = int(np.floor(hmargin / z)) vmargin = int(np.floor(vmargin / z)) # compute output images size roi = [[x, y], [x+w, y], [x+w, y+h], [x, y+h]] pts1 = common.points_apply_homography(H1, roi) x0, y0, w0, h0 = common.bounding_box2D(pts1) # check that the first homography maps the ROI in the positive quadrant np.testing.assert_allclose(np.round([x0, y0]), [hmargin, vmargin], atol=.01) # apply homographies and do the crops common.image_apply_homography(out1, im1, H1, w0 + 2*hmargin, h0 + 2*vmargin) common.image_apply_homography(out2, im2, H2, w0 + 2*hmargin, h0 + 2*vmargin) if cfg['disp_min'] is not None: disp_m = cfg['disp_min'] if cfg['disp_max'] is not None: disp_M = cfg['disp_max'] if block_matching.rectify_secondary_tile_only(cfg['matching_algorithm']): pts_in = [[0, 0], [disp_m, 0], [disp_M, 0]] pts_out = common.points_apply_homography(H1_inv, pts_in) disp_m = pts_out[1,:] - pts_out[0,:] disp_M = pts_out[2,:] - pts_out[0,:] return H1, H2, disp_m, disp_M