def merge_n(output, inputs, offsets, averaging='average_if_close', threshold=1): """ Merge n images of equal sizes by taking the median/mean/min/max pixelwise. Args: inputs: list of paths to the input images output: path to the output image averaging: string containing the name of a function that accepts 1D arrays. It is applied to 1D slices of the stack of images along the last axis. Possible values are, for instance np.min, np.max, np.mean, np.median and their nanproof counterparts, ie np.nanmin, np.nanmax, np.nanmean, np.nanmedian """ assert (len(inputs) == len(offsets)) # get input images size if inputs: with rasterio.open(inputs[0], 'r') as f: h, w = f.shape # read input images and apply offsets x = np.empty((h, w, len(inputs))) for i, img in enumerate(inputs): with rasterio.open(img, 'r') as f: x[:, :, i] = f.read(1) - offsets[i] if cfg['debug']: common.rasterio_write( '{}_registered.tif'.format(os.path.splitext(img)[0]), x[:, :, i] + np.mean(offsets)) # apply the averaging operator if averaging.startswith(('np.', 'numpy.')): avg = np.apply_along_axis(getattr(sys.modules['numpy'], averaging.split('.')[1]), axis=2, arr=x) elif averaging == 'average_if_close': avg = np.apply_along_axis(average_if_close, 2, x, threshold) # add the mean offset avg += np.mean(offsets) # write the average to output if inputs: shutil.copy(inputs[0], output) # copy an input file to get the metadata with rasterio.open(output, 'r+') as f: f.write(np.asarray( [avg]).astype('float32')) # update the output file content
def plys_to_dsm(tile): """ Generates DSM from plyfiles (cloud.ply) Args: tile: a dictionary that provides all you need to process a tile """ out_dsm = os.path.join(tile['dir'], 'dsm.tif') out_conf = os.path.join(tile['dir'], 'confidence.tif') res = cfg['dsm_resolution'] if 'utm_bbx' in cfg: bbx = cfg['utm_bbx'] global_xoff = bbx[0] global_yoff = bbx[3] else: global_xoff = 0 # arbitrary reference global_yoff = 0 xmin, xmax, ymin, ymax = np.loadtxt(os.path.join(tile['dir'], "plyextrema.txt")) if not all(np.isfinite([xmin, xmax, ymin, ymax])): # then the ply is empty return # compute xoff, yoff, xsize, ysize considering final dsm xoff = global_xoff + np.floor((xmin - global_xoff) / res) * res xsize = int(1 + np.floor((xmax - xoff) / res)) yoff = global_yoff + np.ceil((ymax - global_yoff) / res) * res ysize = int(1 - np.floor((ymin - yoff) / res)) roi = xoff, yoff, xsize, ysize clouds = [os.path.join(tile['dir'], n_dir, 'cloud.ply') for n_dir in tile['neighborhood_dirs']] raster, profile = rasterization.plyflatten_from_plyfiles_list(clouds, resolution=res, roi=roi, radius=cfg['dsm_radius'], sigma=cfg['dsm_sigma']) # save output image with utm georeferencing common.rasterio_write(out_dsm, raster[:, :, 0], profile=profile) # export confidence (optional) if raster.shape[-1] == 5: common.rasterio_write(out_conf, raster[:, :, 4], profile=profile)
def disparity_to_height(tile, i): """ Compute a height map from the disparity map of a pair of image tiles. Args: tile: dictionary containing the information needed to process a tile. i: index of the processed pair. """ out_dir = os.path.join(tile['dir'], 'pair_{}'.format(i)) x, y, w, h = tile['coordinates'] print('triangulating tile {} {} pair {}...'.format(x, y, i)) rpc1 = cfg['images'][0]['rpcm'] rpc2 = cfg['images'][i]['rpcm'] H_ref = np.loadtxt(os.path.join(out_dir, 'H_ref.txt')) H_sec = np.loadtxt(os.path.join(out_dir, 'H_sec.txt')) disp = os.path.join(out_dir, 'rectified_disp.tif') mask = os.path.join(out_dir, 'rectified_mask.png') pointing = os.path.join(cfg['out_dir'], 'global_pointing_pair_{}.txt'.format(i)) with rasterio.open(disp, 'r') as f: disp_img = f.read().squeeze() with rasterio.open(mask, 'r') as f: mask_rect_img = f.read().squeeze() height_map = triangulation.height_map(x, y, w, h, rpc1, rpc2, H_ref, H_sec, disp_img, mask_rect_img, A=np.loadtxt(pointing)) # write height map to a file common.rasterio_write(os.path.join(out_dir, 'height_map.tif'), height_map) if cfg['clean_intermediate']: common.remove(H_ref) common.remove(H_sec) common.remove(disp) common.remove(mask)
def plys_to_dsm(tile): """ Generates DSM from plyfiles (cloud.ply) Args: tile: a dictionary that provides all you need to process a tile """ out_dsm = os.path.join(tile['dir'], 'dsm.tif') out_conf = os.path.join(tile['dir'], 'confidence.tif') r = cfg['dsm_resolution'] xmin, xmax, ymin, ymax = np.loadtxt( os.path.join(tile['dir'], "plyextrema.txt")) if not all(np.isfinite([xmin, xmax, ymin, ymax])): # then the ply is empty return # compute xoff, yoff, xsize, ysize on a grid of unit r xoff = np.floor(xmin / r) * r xsize = int(1 + np.floor((xmax - xoff) / r)) yoff = np.ceil(ymax / r) * r ysize = int(1 - np.floor((ymin - yoff) / r)) roi = xoff, yoff, xsize, ysize clouds = [ os.path.join(tile['dir'], n_dir, 'cloud.ply') for n_dir in tile['neighborhood_dirs'] ] raster, profile = plyflatten_from_plyfiles_list(clouds, resolution=r, roi=roi, radius=cfg['dsm_radius'], sigma=cfg['dsm_sigma']) # save output image with utm georeferencing common.rasterio_write(out_dsm, raster[:, :, 0], profile=profile) # export confidence (optional) # note that the plys are assumed to contain the fields: # [x(float32), y(float32), z(float32), r(uint8), g(uint8), b(uint8), confidence(optional, float32)] # so the raster has 4 or 5 columns: [z, r, g, b, confidence (optional)] if raster.shape[-1] == 5: common.rasterio_write(out_conf, raster[:, :, 4], profile=profile)
def plot_matches_low_level(im1, im2, matches): """ Displays two images side by side with matches highlighted Args: im1, im2: paths to the two input images matches: 2D numpy array of size 4xN containing a list of matches (a list of pairs of points, each pair being represented by x1, y1, x2, y2) Returns: path to the resulting image, to be displayed """ # load images with rasterio.open(im1, 'r') as f: img1 = f.read().squeeze() with rasterio.open(im2, 'r') as f: img2 = f.read().squeeze() # transform single channel to 3-channels if img1.ndim < 3: img1 = np.dstack([img1] * 3) if img2.ndim < 3: img2 = np.dstack([img2] * 3) # if images have more than 3 channels, keep only the first 3 if img1.shape[2] > 3: img1 = img1[:, :, 0:3] if img2.shape[2] > 3: img2 = img2[:, :, 0:3] # build the output image h1, w1 = img1.shape[:2] h2, w2 = img2.shape[:2] w = w1 + w2 h = max(h1, h2) out = np.zeros((h, w, 3), np.uint8) out[:h1, :w1] = img1 out[:h2, w1:w] = img2 # define colors, according to min/max intensity values out_min = min(np.nanmin(img1), np.nanmin(img2)) out_max = max(np.nanmax(img1), np.nanmax(img2)) green = [out_min, out_max, out_min] blue = [out_min, out_min, out_max] # plot the matches for i in range(len(matches)): x1 = matches[i, 0] y1 = matches[i, 1] x2 = matches[i, 2] + w1 y2 = matches[i, 3] # convert endpoints to int (nn interpolation) x1, y1, x2, y2 = list(map(int, np.round([x1, y1, x2, y2]))) plot_line(out, x1, y1, x2, y2, blue) try: out[y1, x1] = green out[y2, x2] = green except IndexError: pass # save the output image, and return its path outfile = common.tmpfile('.png') common.rasterio_write(outfile, out) return outfile
for i in range(len(matches)): x1 = matches[i, 0] y1 = matches[i, 1] x2 = matches[i, 2] + w1 y2 = matches[i, 3] # convert endpoints to int (nn interpolation) x1, y1, x2, y2 = list(map(int, np.round([x1, y1, x2, y2]))) plot_line(out, x1, y1, x2, y2, blue) try: out[y1, x1] = green out[y2, x2] = green except IndexError: pass # save the output image, and return its path outfile = common.tmpfile('.png') common.rasterio_write(outfile, out) return outfile def plot_matches(im1, im2, rpc1, rpc2, matches, x=None, y=None, w=None, h=None, outfile=None): """ Plot matches on Pleiades images Args: im1, im2: paths to full Pleiades images rpc1, rpc2: two instances of the RPCModel class, or paths to xml files containing the rpc coefficients matches: 2D numpy array of size 4xN containing a list of matches (a list of pairs of points, each pair being represented by x1, y1, x2, y2). The coordinates are given in the frame of the full images.
def tiles_full_info(tw, th, tiles_txt, create_masks=False): """ List the tiles to process and prepare their output directories structures. Most of the time is spent discarding tiles that are masked by water (according to exogenous dem). Returns: a list of dictionaries. Each dictionary contains the image coordinates and the output directory path of a tile. """ rpc = cfg['images'][0]['rpcm'] roi_msk = cfg['images'][0]['roi'] cld_msk = cfg['images'][0]['cld'] wat_msk = cfg['images'][0]['wat'] rx = cfg['roi']['x'] ry = cfg['roi']['y'] rw = cfg['roi']['w'] rh = cfg['roi']['h'] # list of dictionaries (one for each non-masked tile) tiles = [] # list tiles coordinates tiles_coords, neighborhood_coords_dict = compute_tiles_coordinates( rx, ry, rw, rh, tw, th) if create_masks or not os.path.exists(tiles_txt): print('\ndiscarding masked tiles...') images_sizes = [] for img in cfg['images']: with rasterio.open(img['img'], 'r') as f: images_sizes.append(f.shape) # compute all masks in parallel as numpy arrays tiles_usefulnesses = parallel.launch_calls(is_this_tile_useful, tiles_coords, cfg['max_processes'], images_sizes, tilewise=False, timeout=cfg['timeout']) # discard useless tiles from neighborhood_coords_dict discarded_tiles = set( x for x, (b, _) in zip(tiles_coords, tiles_usefulnesses) if not b) for k, v in neighborhood_coords_dict.items(): neighborhood_coords_dict[k] = list(set(v) - discarded_tiles) for coords, usefulness in zip(tiles_coords, tiles_usefulnesses): useful, mask = usefulness if not useful: continue tile = create_tile(coords, neighborhood_coords_dict) tiles.append(tile) # make tiles directories and store json configuration dumps common.mkdir_p(tile['dir']) for i in range(1, len(cfg['images'])): common.mkdir_p(os.path.join(tile['dir'], 'pair_{}'.format(i))) # save a json dump of the tile configuration tile_cfg = copy.deepcopy(cfg) x, y, w, h = tile['coordinates'] for img in tile_cfg['images']: img.pop('rpcm', None) tile_cfg['roi'] = {'x': x, 'y': y, 'w': w, 'h': h} tile_cfg['full_img'] = False tile_cfg['max_processes'] = 1 tile_cfg['neighborhood_dirs'] = tile['neighborhood_dirs'] tile_cfg['out_dir'] = '../../..' with open(os.path.join(cfg['out_dir'], tile['json']), 'w') as f: json.dump(tile_cfg, f, indent=2, default=workaround_json_int64) # save the mask common.rasterio_write(os.path.join(tile['dir'], 'mask.png'), mask.astype(np.uint8)) else: if len(tiles_coords) == 1: tiles.append(create_tile(tiles_coords[0], neighborhood_coords_dict)) else: with open(tiles_txt, 'r') as f_tiles: for config_json in f_tiles: tile = {} with open( os.path.join(cfg['out_dir'], config_json.rstrip(os.linesep)), 'r') as f_config: tile_cfg = json.load(f_config) roi = tile_cfg['roi'] coords = roi['x'], roi['y'], roi['w'], roi['h'] tiles.append( create_tile(coords, neighborhood_coords_dict)) return tiles
def tiles_full_info(tw, th, tiles_txt, create_masks=False): """ List the tiles to process and prepare their output directories structures. Most of the time is spent discarding tiles that are masked by water (according to exogenous dem). Returns: a list of dictionaries. Each dictionary contains the image coordinates and the output directory path of a tile. """ rpc = cfg['images'][0]['rpc'] roi_msk = cfg['images'][0]['roi'] cld_msk = cfg['images'][0]['cld'] wat_msk = cfg['images'][0]['wat'] rx = cfg['roi']['x'] ry = cfg['roi']['y'] rw = cfg['roi']['w'] rh = cfg['roi']['h'] # build a tile dictionary for all non-masked tiles and store them in a list tiles = [] # list tiles coordinates tiles_coords, neighborhood_coords_dict = compute_tiles_coordinates( rx, ry, rw, rh, tw, th) if os.path.exists(tiles_txt) is False or create_masks is True: print('\ndiscarding masked tiles...') # compute all masks in parallel as numpy arrays tiles_masks = parallel.launch_calls_simple( masking.cloud_water_image_domain, tiles_coords, cfg['max_processes'], rpc, roi_msk, cld_msk, wat_msk) for coords, mask in zip(tiles_coords, tiles_masks): if mask.any(): # there's at least one non-masked pixel in the tile tile = create_tile(coords, neighborhood_coords_dict) tiles.append(tile) # make tiles directories and store json configuration dumps common.mkdir_p(tile['dir']) for i in range(1, len(cfg['images'])): common.mkdir_p( os.path.join(tile['dir'], 'pair_{}'.format(i))) # save a json dump of the tile configuration tile_cfg = copy.deepcopy(cfg) x, y, w, h = tile['coordinates'] tile_cfg['roi'] = {'x': x, 'y': y, 'w': w, 'h': h} tile_cfg['full_img'] = False tile_cfg['max_processes'] = 1 tile_cfg['neighborhood_dirs'] = tile['neighborhood_dirs'] tile_cfg['out_dir'] = '../../..' with open(os.path.join(cfg['out_dir'], tile['json']), 'w') as f: json.dump(tile_cfg, f, indent=2, default=workaround_json_int64) # save the mask common.rasterio_write( os.path.join(tile['dir'], 'cloud_water_image_domain_mask.png'), mask.astype(np.uint8)) else: if len(tiles_coords) == 1: tiles.append(create_tile(tiles_coords[0], neighborhood_coords_dict)) else: with open(tiles_txt, 'r') as f_tiles: for config_json in f_tiles: tile = {} with open( os.path.join(cfg['out_dir'], config_json.rstrip(os.linesep)), 'r') as f_config: tile_cfg = json.load(f_config) roi = tile_cfg['roi'] coords = roi['x'], roi['y'], roi['w'], roi['h'] tiles.append( create_tile(coords, neighborhood_coords_dict)) return tiles