def compute_error(H, x, x1): """ Give this homography, compute the planar reprojection error between points a and b. x and x1 can be n, 2 or n,3 homogeneous coordinates. If only x, y coordinates, assume that z=1, e.g. x, y, 1 for all coordiantes. Parameters ---------- x : ndarray n,2 array of x,y coordinates x1 : ndarray n,2 array of x,y coordinates Returns ------- df : dataframe With columns for x_residual, y_residual, rmse, and error contribution. The dataframe also has cumulative x, t, and total RMS statistics accessible via df.x_rms, df.y_rms, and df.total_rms, respectively. """ if x.shape[1] == 2: x = make_homogeneous(x) if x1.shape[1] == 2: x1 = make_homogeneous(x1) z = np.empty(x.shape) # ToDo: Vectorize for performance for i, j in enumerate(x): z[i] = H.dot(j) z[i] /= z[i][-1] data = np.empty((x.shape[0], 4)) data[:, 0] = x_res = x1[:, 0] - z[:, 0] data[:, 1] = y_res = x1[:, 1] - z[:, 1] if data[:,:1].all() == 0: data[:] = 0.0 total_rms = x_rms = y_rms = 0 else: data[:, 2] = rms = np.sqrt(x_res**2 + y_res**2) total_rms = np.sqrt(np.mean(x_res**2 + y_res**2)) x_rms = np.sqrt(np.mean(x_res**2)) y_rms = np.sqrt(np.mean(y_res**2)) data[:, 3] = rms / total_rms df = pd.DataFrame(data, columns=['x_residuals', 'y_residuals', 'rmse', 'error_contribution']) df.total_rms = total_rms df.x_rms = x_rms df.y_rms = y_rms return df
def compute_error(H, x, x1): """ Give this homography, compute the planar reprojection error between points a and b. x and x1 can be n, 2 or n,3 homogeneous coordinates. If only x, y coordinates, assume that z=1, e.g. x, y, 1 for all coordiantes. Parameters ---------- x : ndarray n,2 array of x,y coordinates x1 : ndarray n,2 array of x,y coordinates Returns ------- df : dataframe With columns for x_residual, y_residual, rmse, and error contribution. The dataframe also has cumulative x, t, and total RMS statistics accessible via df.x_rms, df.y_rms, and df.total_rms, respectively. """ if x.shape[1] == 2: x = make_homogeneous(x) if x1.shape[1] == 2: x1 = make_homogeneous(x1) z = np.empty(x.shape) # ToDo: Vectorize for performance for i, j in enumerate(x): z[i] = H.dot(j) z[i] /= z[i][-1] data = np.empty((x.shape[0], 4)) data[:, 0] = x_res = x1[:, 0] - z[:, 0] data[:, 1] = y_res = x1[:, 1] - z[:, 1] if data[:, :1].all() == 0: data[:] = 0.0 total_rms = x_rms = y_rms = 0 else: data[:, 2] = rms = np.sqrt(x_res**2 + y_res**2) total_rms = np.sqrt(np.mean(x_res**2 + y_res**2)) x_rms = np.sqrt(np.mean(x_res**2)) y_rms = np.sqrt(np.mean(y_res**2)) data[:, 3] = rms / total_rms df = pd.DataFrame( data, columns=['x_residuals', 'y_residuals', 'rmse', 'error_contribution']) df.total_rms = total_rms df.x_rms = x_rms df.y_rms = y_rms return df
def main(msg): fp = shapely.wkt.loads(msg['poly']) files = msg['files'] # Compute the fundamental matrices fundamentals = {} matches = [] for k, v in msg['matches'].items(): edge = eval(k) print(edge) match = pd.read_json(v) s = match.iloc[0].source d = match.iloc[0].destination source_path = files[str(edge[0])] destination_path = files[str(edge[1])] x1 = from_hdf(source_path, index=match.source_idx.values, descriptors=False) x2 = from_hdf(destination_path, index=match.destination_idx.values, descriptors=False) x1 = make_homogeneous(x1[['x', 'y']].values) x2 = make_homogeneous(x2[['x', 'y']].values) f, fmask = compute_fundamental_matrix(x1, x2, method='ransac', reproj_threshold=20) fundamentals[edge] = f match['strength'] = compute_reprojection_error(f, x1, x2) matches.append(match) matches = pd.concat(matches) # Of the concatenated matches only a subset intersect the geometry for this overlap, pull these def check_in(r, poly): p = shapely.geometry.Point(r.lon, r.lat) return p.within(poly) intersects = matches.apply(check_in, args=(fp,), axis=1) matches = matches[intersects] matches = matches.reset_index(drop=True) # Apply the spatial suppression bounds = fp.bounds k = fp.area / 0.005 if k < 3: k = 3 if k > 25: k = 25 subset = spatial_suppression(matches, bounds, k=k) # Push the points through overlaps = msg['overlaps'] oid = msg['oid'] pts = deepen(subset, fundamentals, overlaps, oid) return pts
def compute_fundamental_error(F, x, x1): """ Compute the fundamental error using the idealized error metric. Ideal error is defined by $x^{\intercal}Fx = 0$, where $x$ are all matchpoints in a given image and $x^{\intercal}F$ defines the standard form of the epipolar line in the second image. This method assumes that x and x1 are ordered such that x[0] correspondes to x1[0]. Parameters ---------- F : ndarray (3,3) Fundamental matrix x : arraylike (n,2) or (n,3) array of homogeneous coordinates x1 : arraylike (n,2) or (n,3) array of homogeneous coordinates with the same length as argument x Returns ------- F_error : ndarray n,1 vector of reprojection errors """ # TODO: Can this be vectorized for performance? if x.shape[1] != 3: x = make_homogeneous(x) if x1.shape[1] != 3: x1 = make_homogeneous(x1) if isinstance(x, pd.DataFrame): x = x.values if isinstance(x1, pd.DataFrame): x1 = x1.values err = np.empty(len(x)) for i in range(len(x)): err[i] = x1[i].T.dot(F).dot(x[i]) return err
def compute_error(self, a, b, mask=None): """ Give this homography, compute the planar reprojection error between points a and b. Parameters ---------- a : ndarray n,2 array of x,y coordinates b : ndarray n,2 array of x,y coordinates index : ndarray Index to be used in the returned dataframe Returns ------- df : dataframe With columns for x_residual, y_residual, rmse, and error contribution. The dataframe also has cumulative x, t, and total RMS statistics accessible via df.x_rms, df.y_rms, and df.total_rms, respectively. """ if mask is not None: mask = mask else: mask = pd.Series(True, index=self.index) a = a[mask].values b = b[mask].values if a.shape[1] == 2: a = make_homogeneous(a) if b.shape[1] == 2: b = make_homogeneous(b) # ToDo: Vectorize for performance for i, j in enumerate(a): a[i] = self.dot(j) a[i] /= a[i][-1] data = np.empty((a.shape[0], 4)) data[:, 0] = x_res = b[:, 0] - a[:, 0] data[:, 1] = y_res = b[:, 1] - a[:, 1] data[:, 2] = rms = np.sqrt(x_res**2 + y_res**2) total_rms = np.sqrt(np.mean(x_res**2 + y_res**2)) x_rms = np.sqrt(np.mean(x_res**2)) y_rms = np.sqrt(np.mean(y_res**2)) data[:, 3] = rms / total_rms df = pd.DataFrame(data, columns=['x_residuals', 'y_residuals', 'rmse', 'error_contribution'], index=self.index) df.total_rms = total_rms df.x_rms = x_rms df.y_rms = y_rms return df
def compute_fundamental_matrix(kp1, kp2, method='mle', reproj_threshold=2.0, confidence=0.99, mle_reproj_threshold=0.5): """ Given two arrays of keypoints compute the fundamental matrix. This function accepts two dataframe of keypoints that have Parameters ---------- kp1 : arraylike (n, 2) of coordinates from the source image kp2 : ndarray (n, 2) of coordinates from the destination image method : {'ransac', 'lmeds', 'normal', '8point'} The openCV algorithm to use for outlier detection reproj_threshold : float The maximum distances in pixels a reprojected points can be from the epipolar line to be considered an inlier confidence : float [0, 1] that the estimated matrix is correct Returns ------- F : ndarray A 3x3 fundamental matrix mask : pd.Series A boolean mask identifying those points that are valid. Notes ----- While the method is user definable, if the number of input points is < 7, normal outlier detection is automatically used, if 7 > n > 15, least medians is used, and if 7 > 15, ransac can be used. """ if method == 'mle': # Grab an initial estimate using RANSAC, then apply MLE method_ = cv2.FM_RANSAC elif method == 'ransac': method_ = cv2.FM_RANSAC elif method == 'lmeds': method_ = cv2.FM_LMEDS elif method == 'normal': method_ = cv2.FM_7POINT elif method == '8point': method_ = cv2.FM_8POINT else: raise ValueError("Unknown estimation method. Choices are: 'lme', 'ransac', 'lmeds', '8point', or 'normal'.") # OpenCV wants arrays try: # OpenCV < 3.4.1 F, mask = cv2.findFundamentalMat(np.asarray(kp1), np.asarray(kp2), method_, param1=reproj_threshold, param2=confidence) except: # OpenCV >= 3.4.1 F, mask = cv2.findFundamentalMat(np.asarray(kp1), np.asarray(kp2), method_, ransacReprojThreshold=reproj_threshold, confidence=confidence) if F is None: warnings.warn("F Computation Failed.") return None, None if F.shape != (3,3): warnings.warn('F computation fell back to 7-point algorithm, not setting F.') return None, None # Ensure that the singularity constraint is met F = enforce_singularity_constraint(F) try: mask = mask.astype(bool).ravel() # Enforce dimensionality except: return # pragma: no cover if method == 'mle': # Now apply the gold standard algorithm to refine F if kp1.shape[1] != 3: kp1 = make_homogeneous(kp1) if kp2.shape[1] != 3: kp2 = make_homogeneous(kp2) # Generate an idealized and to be updated camera model p1 = camera.camera_from_f(F) p = camera.idealized_camera() if kp1[mask].shape[0] <=12 or kp2[mask].shape[0] <=12: warnings.warn("Unable to apply MLE. Not enough correspondences. Returning with a RANSAC computed F matrix.") return F, mask # Apply Levenber-Marquardt to perform a non-linear lst. squares fit # to minimize triangulation error (this is a local bundle) result = optimize.least_squares(camera.projection_error, p1.ravel(), args=(p, kp1[mask].T, kp2[mask].T), method='lm') gold_standard_p = result.x.reshape(3, 4) # SciPy Lst. Sq. requires a vector, camera is 3x4 optimality = result.optimality gold_standard_f = camera_utils.crossform(gold_standard_p[:,3]).dot(gold_standard_p[:,:3]) F = gold_standard_f mask = update_fundamental_mask(F, kp1, kp2, threshold=mle_reproj_threshold).values return F, mask
def deepen(matches, fundamentals, overlaps, oid): points = [] for g, subm in matches.groupby(['source', 'destination']): w = int(g[0]) v = int(g[1]) push_into = [i for i in overlaps if i not in g] x1 = make_homogeneous(subm[['source_x', 'source_y']].values) x2 = make_homogeneous(subm[['destination_x', 'destination_y']].values) pid = 0 for i in range(x1.shape[0]): row = subm.iloc[i] geom = 'SRID=949900;POINTZ({} {} {})'.format(row.lon, row.lat, 0) a = x1[i] b = x2[i] p1 = {'image_id':w, 'keypoint_id':int(row.source_idx), 'x':float(a[0]), 'y':float(a[1]), 'match_id':int(row.name), 'point_id':'{}_{}_{}'.format(oid, g, pid), 'geom':geom} p2 = {'image_id':v, 'keypoint_id':int(row.destination_idx), 'x':float(b[0]), 'y':float(b[1]), 'match_id':int(row.name), 'point_id':'{}_{}_{}'.format(oid, g, pid), 'geom':geom} points.append(p1) points.append(p2) for e in push_into: try: if w > e: f31 = [e,w] f31 = np.asarray(fundamentals[tuple(f31)]).T else: f31 = [w,e] f31 = np.asarray(fundamentals[tuple(f31)]) if v > e: f32 = [e,v] f32 = np.asarray(fundamentals[tuple(f32)]).T else: f32 = [v,e] f32 = np.asarray(fundamentals[tuple(f32)]) x3 = np.cross(f31.dot(a), f32.dot(b)) x3[0] /= x3[2] x3[1] /= x3[2] # This needs to aggregate all of the n = {'image_id':e, 'keypoint_id':None, 'x':float(x3[0]), 'y':float(x3[1]), 'point_id':'{}_{}_{}'.format(oid, g, pid), 'geom':geom, 'match_id':None} points.append(n) except: pass pid += 1 return points
def compute_error(self, a, b, mask=None): """ Give this homography, compute the planar reprojection error between points a and b. Parameters ---------- a : ndarray n,2 array of x,y coordinates b : ndarray n,2 array of x,y coordinates index : ndarray Index to be used in the returned dataframe Returns ------- df : dataframe With columns for x_residual, y_residual, rmse, and error contribution. The dataframe also has cumulative x, t, and total RMS statistics accessible via df.x_rms, df.y_rms, and df.total_rms, respectively. """ if mask is not None: mask = mask else: mask = pd.Series(True, index=self.index) a = a[mask].values b = b[mask].values if a.shape[1] == 2: a = make_homogeneous(a) if b.shape[1] == 2: b = make_homogeneous(b) # ToDo: Vectorize for performance for i, j in enumerate(a): a[i] = self.dot(j) a[i] /= a[i][-1] data = np.empty((a.shape[0], 4)) data[:, 0] = x_res = b[:, 0] - a[:, 0] data[:, 1] = y_res = b[:, 1] - a[:, 1] data[:, 2] = rms = np.sqrt(x_res**2 + y_res**2) total_rms = np.sqrt(np.mean(x_res**2 + y_res**2)) x_rms = np.sqrt(np.mean(x_res**2)) y_rms = np.sqrt(np.mean(y_res**2)) data[:, 3] = rms / total_rms df = pd.DataFrame(data, columns=[ 'x_residuals', 'y_residuals', 'rmse', 'error_contribution' ], index=self.index) df.total_rms = total_rms df.x_rms = x_rms df.y_rms = y_rms return df