def height_map_to_point_cloud(cloud, heights, rpc, off_x=None, off_y=None, crop_colorized=''): """ Computes a color point cloud from a height map. Args: cloud: path to the output points cloud (ply format) heights: height map, sampled on the same grid as the crop_colorized image. In particular, its size is the same as crop_colorized. rpc: instances of the rpcm.RPCModel class off_{x,y} (optional, default None): coordinates of the origin of the crop we are dealing with in the pixel coordinates of the original full size image crop_colorized (optional, default ''): path to a colorized crop of a Pleiades image """ with rasterio.open(heights) as src: h_map = src.read(1) h, w = h_map.shape heights = h_map.ravel() indices = np.indices((h, w)) non_nan_ind = np.where(~np.isnan(heights))[0] alts = heights[non_nan_ind] cols = indices[1].ravel()[non_nan_ind] rows = indices[0].ravel()[non_nan_ind] if off_x or off_y: cols = cols + (off_x or 0) rows = rows + (off_y or 0) # localize pixels lons = np.empty_like(heights, dtype=np.float64) lats = np.empty_like(heights, dtype=np.float64) lons[non_nan_ind], lats[non_nan_ind] = rpc.localization(cols, rows, alts) # output CRS conversion in_crs = geographiclib.pyproj_crs("epsg:4979") out_crs = geographiclib.pyproj_crs(cfg['out_crs']) proj_com = "CRS {}".format(cfg['out_crs']) if out_crs != in_crs: x, y, z = geographiclib.pyproj_transform(lons, lats, in_crs, out_crs, heights) else: x, y, z = lons, lats, heights xyz_array = np.column_stack((x, y, z)).reshape(h, w, 3) filter_xyz_and_write_to_ply(cloud, xyz_array, cfg['3d_filtering_r'], cfg['3d_filtering_n'], cfg['gsd'], crop_colorized, proj_com)
def stereo_corresp_to_xyz(rpc1, rpc2, pts1, pts2, out_crs=None): """ Compute a point cloud from stereo correspondences between two images using RPC camera models. No need to go through the disparity map Args: rpc1, rpc2 (rpcm.RPCModel): camera models pts1, pts2 (arrays): 2D arrays of shape (N, 2) containing the image coordinates of N 2d keypoints matched beween im1 and im2, i.e. cooridnates in the same row of these arrays back-project to the same 3D point out_crs (pyproj.crs.CRS): object defining the desired coordinate reference system for the output xyz map Returns: xyz: array of shape (h, w, 3) where each pixel contains the 3D coordinates of the triangulated point in the coordinate system defined by `out_crs` err: array of shape (h, w) where each pixel contains the triangulation error """ # copy rpc coefficients to an RPCStruct object rpc1_c_struct = RPCStruct(rpc1, delta=0.1) rpc2_c_struct = RPCStruct(rpc2, delta=0.1) # get number of points to triangulate n = pts1.shape[0] # define the argument types of the stereo_corresp_to_lonlatalt function from disp_to_h.so lib.stereo_corresp_to_lonlatalt.argtypes = (ndpointer(dtype=c_double, shape=(n, 3)), ndpointer(dtype=c_float, shape=(n, 1)), ndpointer(dtype=c_float, shape=(n, 2)), ndpointer(dtype=c_float, shape=(n, 2)), c_int, POINTER(RPCStruct), POINTER(RPCStruct)) # call the stereo_corresp_to_lonlatalt function from disp_to_h.so lonlatalt = np.zeros((n, 3), dtype='float64') err = np.zeros((n, 1), dtype='float32') lib.stereo_corresp_to_lonlatalt(lonlatalt, err, pts1.astype('float32'), pts2.astype('float32'), n, byref(rpc1_c_struct), byref(rpc2_c_struct)) # output CRS conversion in_crs = geographiclib.pyproj_crs("epsg:4979") if out_crs and out_crs != in_crs: x, y, z = geographiclib.pyproj_transform(lonlatalt[:, 0], lonlatalt[:, 1], in_crs, out_crs, lonlatalt[:, 2]) xyz_array = np.column_stack((x, y, z)).astype(np.float32) else: xyz_array = lonlatalt return xyz_array, err
def build_cfg(user_cfg): """ Populate a dictionary containing the s2p parameters from a user config file. This dictionary is contained in the global variable 'cfg' of the config module. Args: user_cfg: user config dictionary """ # check that all the mandatory arguments are defined check_parameters(user_cfg) # fill the config module: updates the content of the config.cfg dictionary # with the content of the user_cfg dictionary cfg.update(user_cfg) # set keys 'clr', 'cld' and 'roi' of the reference image to None if they # are not already defined. The default values of these optional arguments # can not be defined directly in the config.py module. They would be # overwritten by the previous update, because they are in a nested dict. cfg['images'][0].setdefault('clr') cfg['images'][0].setdefault('cld') cfg['images'][0].setdefault('roi') cfg['images'][0].setdefault('wat') # make sure that input data have absolute paths for i in range(len(cfg['images'])): for d in ['clr', 'cld', 'roi', 'wat', 'img']: if d in cfg['images'][i] and cfg['images'][i][ d] is not None and not os.path.isabs(cfg['images'][i][d]): cfg['images'][i][d] = os.path.abspath(cfg['images'][i][d]) # get out_crs if 'out_crs' not in cfg or cfg['out_crs'] is None: x, y, w, h = [cfg['roi'][k] for k in ['x', 'y', 'w', 'h']] utm_zone = rpc_utils.utm_zone(cfg['images'][0]['rpcm'], x, y, w, h) epsg_code = geographiclib.epsg_code_from_utm_zone(utm_zone) cfg['out_crs'] = "epsg:{}".format(epsg_code) geographiclib.pyproj_crs(cfg['out_crs']) # get image ground sampling distance cfg['gsd'] = rpc_utils.gsd_from_rpc(cfg['images'][0]['rpcm'])
def global_dsm(tiles): """ """ out_dsm_vrt = os.path.join(cfg['out_dir'], 'dsm.vrt') out_dsm_tif = os.path.join(cfg['out_dir'], 'dsm.tif') dsms_list = [os.path.join(t['dir'], 'dsm.tif') for t in tiles] dsms = '\n'.join(d for d in dsms_list if os.path.exists(d)) input_file_list = os.path.join(cfg['out_dir'], 'gdalbuildvrt_input_file_list.txt') with open(input_file_list, 'w') as f: f.write(dsms) common.run("gdalbuildvrt -vrtnodata nan -input_file_list %s %s" % (input_file_list, out_dsm_vrt)) res = cfg['dsm_resolution'] if 'roi_geojson' in cfg: ll_poly = geographiclib.read_lon_lat_poly_from_geojson( cfg['roi_geojson']) pyproj_crs = geographiclib.pyproj_crs(cfg['out_crs']) bbx = geographiclib.crs_bbx(ll_poly, pyproj_crs) xoff = bbx[0] yoff = bbx[3] xsize = int(np.ceil((bbx[1] - bbx[0]) / res)) ysize = int(np.ceil((bbx[3] - bbx[2]) / res)) projwin = "-projwin {} {} {} {}".format(xoff, yoff, xoff + xsize * res, yoff - ysize * res) else: projwin = "" common.run(" ".join([ "gdal_translate", "-co", "TILED=YES", "-co", "COMPRESS=DEFLATE", "-co", "PREDICTOR=2", "-co", "BIGTIFF=IF_SAFER", projwin, out_dsm_vrt, out_dsm_tif ])) # EXPORT CONFIDENCE out_conf_vrt = os.path.join(cfg['out_dir'], 'confidence.vrt') out_conf_tif = os.path.join(cfg['out_dir'], 'confidence.tif') dsms_list = [os.path.join(t['dir'], 'confidence.tif') for t in tiles] dems_list_ok = [d for d in dsms_list if os.path.exists(d)] dsms = '\n'.join(dems_list_ok) input_file_list = os.path.join(cfg['out_dir'], 'gdalbuildvrt_input_file_list2.txt') if len(dems_list_ok) > 0: with open(input_file_list, 'w') as f: f.write(dsms) common.run("gdalbuildvrt -vrtnodata nan -input_file_list %s %s" % (input_file_list, out_conf_vrt)) common.run(" ".join([ "gdal_translate", "-co TILED=YES -co BIGTIFF=IF_SAFER", "%s %s %s" % (projwin, out_conf_vrt, out_conf_tif) ]))
def disparity_to_ply(tile): """ Compute a point cloud from the disparity map of a pair of image tiles. This function is called by s2p.main only if there are two input images (not three). Args: tile: dictionary containing the information needed to process a tile. """ out_dir = 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]['rpcm'] rpc2 = cfg['images'][1]['rpcm'] print('triangulating tile {} {}...'.format(x, y)) 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') extra = os.path.join(out_dir, 'pair_1', 'rectified_disp_confidence.tif') if not os.path.exists(extra): # confidence file not always generated extra = '' mask_rect = os.path.join(out_dir, 'pair_1', 'rectified_mask.png') mask_orig = os.path.join(out_dir, '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) # we want rectified_ref.png and rectified_ref.tif to have the same size with rasterio.open(os.path.join(out_dir, 'pair_1', 'rectified_ref.tif')) as f: ww, hh = f.width, f.height common.image_apply_homography(colors, cfg['images'][0]['clr'], hom, ww, hh) else: common.image_qauto( os.path.join(out_dir, 'pair_1', 'rectified_ref.tif'), colors) # compute the point cloud with rasterio.open(disp, 'r') as f: disp_img = f.read().squeeze() with rasterio.open(mask_rect, 'r') as f: mask_rect_img = f.read().squeeze() pyproj_out_crs = geographiclib.pyproj_crs(cfg['out_crs']) proj_com = "CRS {}".format(cfg['out_crs']) xyz_array, err = triangulation.disp_to_xyz(rpc1, rpc2, np.loadtxt(H_ref), np.loadtxt(H_sec), disp_img, mask_rect_img, pyproj_out_crs, img_bbx=(x, x + w, y, y + h), A=np.loadtxt(pointing)) triangulation.filter_xyz_and_write_to_ply(ply_file, xyz_array, cfg['3d_filtering_r'], cfg['3d_filtering_n'], cfg['gsd'], colors, proj_com, confidence=extra) # 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 disp_to_xyz(rpc1, rpc2, H1, H2, disp, mask, out_crs=None, img_bbx=None, A=None): """ Compute a height map from a disparity map, using RPC camera models. Args: rpc1, rpc2 (rpcm.RPCModel): camera models H1, H2 (arrays): 3x3 numpy arrays defining the rectifying homographies disp, mask (array): 2D arrays of shape (h, w) representing the diparity and mask maps out_crs (pyproj.crs.CRS): object defining the desired coordinate reference system for the output xyz map img_bbx (4-tuple): col_min, col_max, row_min, row_max defining the unrectified image domain to process. A (array): 3x3 array with the pointing correction matrix for im2 Returns: xyz: array of shape (h, w, 3) where each pixel contains the 3D coordinates of the triangulated point in the coordinate system defined by `out_crs` err: array of shape (h, w) where each pixel contains the triangulation error """ # copy rpc coefficients to an RPCStruct object rpc1_c_struct = RPCStruct(rpc1) rpc2_c_struct = RPCStruct(rpc2) # handle optional arguments if A is not None: # apply pointing correction H2 = np.dot(H2, np.linalg.inv(A)) if img_bbx is None: img_bbx = (-np.inf, np.inf, -np.inf, np.inf) # define the argument types of the disp_to_lonlatalt function from disp_to_h.so h, w = disp.shape lib.disp_to_lonlatalt.argtypes = (ndpointer(dtype=c_double, shape=(h, w, 3)), ndpointer(dtype=c_float, shape=(h, w)), ndpointer(dtype=c_float, shape=(h, w)), ndpointer(dtype=c_float, shape=(h, w)), ndpointer(dtype=c_float, shape=(h, w)), c_int, c_int, ndpointer(dtype=c_double, shape=(9, )), ndpointer(dtype=c_double, shape=(9, )), POINTER(RPCStruct), POINTER(RPCStruct), ndpointer(dtype=c_float, shape=(4, ))) # call the disp_to_lonlatalt function from disp_to_h.so lonlatalt = np.zeros((h, w, 3), dtype='float64') err = np.zeros((h, w), dtype='float32') dispx = disp.astype('float32') dispy = np.zeros((h, w), dtype='float32') msk = mask.astype('float32') lib.disp_to_lonlatalt(lonlatalt, err, dispx, dispy, msk, w, h, H1.flatten(), H2.flatten(), byref(rpc1_c_struct), byref(rpc2_c_struct), np.asarray(img_bbx, dtype='float32')) # output CRS conversion in_crs = geographiclib.pyproj_crs("epsg:4979") if out_crs and out_crs != in_crs: # reshape the lonlatlat array into a 3-column 2D-array lonlatalt = lonlatalt.reshape(-1, 3) x, y, z = geographiclib.pyproj_transform(lonlatalt[:, 0], lonlatalt[:, 1], in_crs, out_crs, lonlatalt[:, 2]) xyz_array = np.column_stack((x, y, z)).reshape(h, w, 3).astype(np.float32) else: xyz_array = lonlatalt return xyz_array, err