def loop_zhang(F, w, h): """ Computes rectifying homographies from a fundamental matrix, with Loop-Zhang. Args: F: 3x3 numpy array containing the fundamental matrix w, h: images size. The two images are supposed to have same size Returns: The two rectifying homographies. The rectifying homographies are computed using the Pascal Monasse binary named rectify_mindistortion. It uses the Loop-Zhang algorithm. """ Ffile = common.tmpfile('.txt') Haf = common.tmpfile('.txt') Hbf = common.tmpfile('.txt') common.matrix_write(Ffile, F) common.run('rectify_mindistortion %s %d %d %s %s > /dev/null' % (Ffile, w, h, Haf, Hbf)) Ha = common.matrix_read(Haf, size=(3, 3)) Hb = common.matrix_read(Hbf, size=(3, 3)) # check if both the images are rotated a = does_this_homography_change_the_vertical_direction(Ha) b = does_this_homography_change_the_vertical_direction(Hb) if a and b: R = np.array([[-1, 0, 0], [0, -1, 0], [0, 0, 1]]) Ha = np.dot(R, Ha) Hb = np.dot(R, Hb) return Ha, Hb
def plot_vectors(p, v, x, y, w, h, f=1, out_file=None): """ Plots vectors on an image, using gnuplot Args: p: points (origins of vectors),represented as a numpy Nx2 array v: vectors, represented as a numpy Nx2 array x, y, w, h: rectangular ROI f: (optional, default is 1) exageration factor out_file: (optional, default is None) path to the output file Returns: nothing, but opens a display or write a png file """ tmp = common.tmpfile('.txt') data = np.hstack((p, v)) np.savetxt(tmp, data, fmt='%6f') gp_string = 'set term png size %d,%d;unset key;unset tics;plot [%d:%d] [%d:%d] "%s" u($1):($2):(%d*$3):(%d*$4) w vectors head filled' % ( w, h, x, x + w, y, y + h, tmp, f, f) if out_file is None: out_file = common.tmpfile('.png') common.run("gnuplot -p -e '%s' > %s" % (gp_string, out_file)) print(out_file) if out_file is None: os.system("v %s &" % out_file)
def plot_vectors(p, v, x, y, w, h, f=1, out_file=None): """ Plots vectors on an image, using gnuplot Args: p: points (origins of vectors),represented as a numpy Nx2 array v: vectors, represented as a numpy Nx2 array x, y, w, h: rectangular ROI f: (optional, default is 1) exageration factor out_file: (optional, default is None) path to the output file Returns: nothing, but opens a display or write a png file """ tmp = common.tmpfile('.txt') data = np.hstack((p, v)) np.savetxt(tmp, data, fmt='%6f') gp_string = 'set term png size %d,%d;unset key;unset tics;plot [%d:%d] [%d:%d] "%s" u($1):($2):(%d*$3):(%d*$4) w vectors head filled' % (w, h, x, x+w, y, y+h, tmp, f, f) if out_file is None: out_file = common.tmpfile('.png') common.run("gnuplot -p -e '%s' > %s" % (gp_string, out_file)) print(out_file) if out_file is None: os.system("v %s &" % out_file)
def cloud_water_image_domain(x, y, w, h, rpc, roi_gml=None, cld_gml=None, wat_msk=None): """ Compute a mask for pixels masked by clouds, water, or out of image domain. Args: x, y, w, h: coordinates of the ROI roi_gml (optional): path to a gml file containing a mask defining the area contained in the full image cld_gml (optional): path to a gml file containing a mask defining the areas covered by clouds Returns: 2D array containing the output binary mask. 0 indicate masked pixels, 1 visible pixels. """ # coefficients of the transformation associated to the crop H = common.matrix_translation(-x, -y) hij = ' '.join([str(el) for el in H.flatten()]) mask = np.ones((h, w), dtype=np.bool) if roi_gml is not None: # image domain mask (polygons) tmp = common.tmpfile('.png') subprocess.check_call('cldmask %d %d -h "%s" %s %s' % (w, h, hij, roi_gml, tmp), shell=True) f = gdal.Open(tmp) mask = np.logical_and(mask, f.ReadAsArray()) f = None # this is the gdal way of closing files if not mask.any(): return mask if cld_gml is not None: # cloud mask (polygons) tmp = common.tmpfile('.png') subprocess.check_call('cldmask %d %d -h "%s" %s %s' % (w, h, hij, cld_gml, tmp), shell=True) f = gdal.Open(tmp) mask = np.logical_and(mask, ~f.ReadAsArray().astype(bool)) f = None # this is the gdal way of closing files if not mask.any(): return mask if wat_msk is not None: # water mask (raster) f = gdal.Open(wat_msk) mask = np.logical_and(mask, f.ReadAsArray(x, y, w, h)) f = None # this is the gdal way of closing files return mask
def height_map(out, x, y, w, h, z, rpc1, rpc2, H1, H2, disp, mask, rpc_err, out_filt, A=None): """ Computes an altitude map, on the grid of the original reference image, from a disparity map given on the grid of the rectified reference image. Args: out: path to the output file x, y, w, h: four integers defining the rectangular ROI in the original image. (x, y) is the top-left corner, and (w, h) are the dimensions of the rectangle. z: zoom factor (usually 1, 2 or 4) used to produce the input disparity map rpc1, rpc2: paths to the xml files H1, H2: path to txt files containing two 3x3 numpy arrays defining the rectifying homographies disp, mask: paths to the diparity and mask maps rpc_err: path to the output rpc_error of triangulation A (optional): path to txt file containing the pointing correction matrix for im2 """ tmp = common.tmpfile('.tif') height_map_rectified(rpc1, rpc2, H1, H2, disp, mask, tmp, rpc_err, A) transfer_map(tmp, H1, x, y, w, h, z, out) # apply output filter common.run('plambda {0} {1} "x 0 > y nan if" -o {1}'.format(out_filt, out))
def height_map_rectified(rpc1, rpc2, H1, H2, disp, mask, height, rpc_err, A=None): """ Computes a height map from a disparity map, using rpc. Args: rpc1, rpc2: paths to the xml files H1, H2: path to txt files containing two 3x3 numpy arrays defining the rectifying homographies disp, mask: paths to the diparity and mask maps height: path to the output height map rpc_err: path to the output rpc_error of triangulation A (optional): path to txt file containing the pointing correction matrix for im2 """ if A is not None: HH2 = common.tmpfile('.txt') np.savetxt(HH2, np.dot(np.loadtxt(H2), np.linalg.inv(np.loadtxt(A)))) else: HH2 = H2 common.run("disp_to_h %s %s %s %s %s %s %s %s" % (rpc1, rpc2, H1, HH2, disp, mask, height, rpc_err))
def image_keypoints(im, x, y, w, h, max_nb=None, extra_params=''): """ Runs SIFT (the keypoints detection and description only, no matching). It uses Ives Rey Otero's implementation published in IPOL: http://www.ipol.im/pub/pre/82/ Args: im: path to the input image max_nb (optional): maximal number of keypoints. If more keypoints are detected, those at smallest scales are discarded extra_params (optional): extra parameters to be passed to the sift binary Returns: path to the file containing the list of descriptors """ keyfile = common.tmpfile('.txt') if max_nb: cmd = "sift_roi %s %d %d %d %d --max-nb-pts %d %s -o %s" % ( im, x, y, w, h, max_nb, extra_params, keyfile) else: cmd = "sift_roi %s %d %d %d %d %s -o %s" % (im, x, y, w, h, extra_params, keyfile) common.run(cmd) return keyfile
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 img1 = piio.read(im1).astype(np.uint8) img2 = piio.read(im2).astype(np.uint8) # 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') piio.write(outfile, out) return outfile
def fundamental_matrix_ransac(matches, precision=1.0, return_inliers=False): """ Estimates the fundamental matrix given a set of point correspondences between two images, using ransac. Arguments: matches: numpy 2D array of size Nx4 containing a list of pair of matching points. Each line is of the form x1, y1, x2, y2, where (x1, y1) is the point in the first view while (x2, y2) is the matching point in the second view. It can be the path to a txt file containing such an array. precision: optional parameter indicating the maximum error allowed for counting the inliers return_inliers: optional boolean flag to activate/deactivate inliers output Returns: the estimated fundamental matrix, and optionally the 2D array containing the inliers The algorithm uses ransac as a search engine. """ if type(matches) is np.ndarray: # write a file containing the list of correspondences. The # expected format is a text file with one match per line: x1 y1 x2 y2 matchfile = common.tmpfile('.txt') np.savetxt(matchfile, matches) else: # assume it is a path to a txt file containing the matches matchfile = matches # call ransac binary, from Enric's imscript inliers = common.tmpfile('.txt') Ffile = common.tmpfile('.txt') awk_command = "awk {\'printf(\"%e %e %e\\n%e %e %e\\n%e %e %e\", $3, $4, $5, $6, $7, $8, $9, $10, $11)\'}" common.run("ransac fmn 1000 %f 7 %s < %s | grep param | %s > %s" % (precision, inliers, matchfile, awk_command, Ffile)) if return_inliers: return np.loadtxt(Ffile).transpose(), np.loadtxt(inliers) else: return np.loadtxt(Ffile).transpose()
def keypoints_match(k1, k2, method='relative', sift_thresh=0.6, F=None, model=None, epipolar_threshold=10): """ Find matches among two lists of sift keypoints. Args: k1, k2: paths to text files containing the lists of sift descriptors method (optional, default is 'relative'): flag ('relative' or 'absolute') indicating wether to use absolute distance or relative distance sift_thresh (optional, default is 0.6): threshold for distance between SIFT descriptors. These descriptors are 128-vectors, whose coefficients range from 0 to 255, thus with absolute distance a reasonable value for this threshold is between 200 and 300. With relative distance (ie ratio between distance to nearest and distance to second nearest), the commonly used value for the threshold is 0.6. F (optional): affine fundamental matrix model (optional, default is None): model imposed by RANSAC when searching the set of inliers. If None all matches are considered as inliers. Returns: if any, a numpy 2D array containing the list of inliers matches. """ # compute matches mfile = common.tmpfile('.txt') cmd = "matching %s %s -%s %f -o %s" % (k1, k2, method, sift_thresh, mfile) if F is not None: fij = ' '.join( str(x) for x in [F[0, 2], F[1, 2], F[2, 0], F[2, 1], F[2, 2]]) cmd = "%s -f \"%s\"" % (cmd, fij) cmd += " --epipolar-threshold {}".format(epipolar_threshold) common.run(cmd) matches = np.loadtxt(mfile) if matches.ndim == 2: # filter outliers with ransac if model == 'fundamental' and len(matches) >= 7: common.run("ransac fmn 1000 .3 7 %s < %s" % (mfile, mfile)) elif model == 'homography' and len(matches) >= 4: common.run("ransac hom 1000 1 4 /dev/null /dev/null %s < %s" % (mfile, mfile)) elif model == 'hom_fund' and len(matches) >= 7: common.run("ransac hom 1000 2 4 /dev/null /dev/null %s < %s" % (mfile, mfile)) common.run("ransac fmn 1000 .2 7 %s < %s" % (mfile, mfile)) if os.stat(mfile).st_size > 0: # return numpy array of matches return np.loadtxt(mfile)
def register_heights(im1, im2): """ Affine registration of heights. Args: im1: first height map im2: second height map, to be registered on the first one Returns path to the registered second height map """ # remove high frequencies with a morphological zoom out im1_low_freq = common.image_zoom_out_morpho(im1, 4) im2_low_freq = common.image_zoom_out_morpho(im2, 4) # first read the images and store them as numpy 1D arrays, removing all the # nans and inf i1 = piio.read(im1_low_freq).ravel() #np.ravel() gives a 1D view i2 = piio.read(im2_low_freq).ravel() ind = np.logical_and(np.isfinite(i1), np.isfinite(i2)) h1 = i1[ind] h2 = i2[ind] # for debug print(np.shape(i1)) print(np.shape(h1)) # # 1st option: affine # # we search the (u, v) vector that minimizes the following sum (over # # all the pixels): # #\sum (im1[i] - (u*im2[i]+v))^2 # # it is a least squares minimization problem # A = np.vstack((h2, h2*0+1)).T # b = h1 # z = np.linalg.lstsq(A, b)[0] # u = z[0] # v = z[1] # # # apply the affine transform and return the modified im2 # out = common.tmpfile('.tif') # common.run('plambda %s "x %f * %f +" > %s' % (im2, u, v, out)) # 2nd option: translation only v = np.mean(h1 - h2) out = common.tmpfile('.tif') common.run('plambda %s "x %f +" -o %s' % (im2, v, out)) return out
def compute_disparity_map(im1, im2, disp, mask, algo, disp_min=None, disp_max=None, extra_params=''): """ Runs a block-matching binary on a pair of stereo-rectified images. Args: im1, im2: rectified stereo pair disp: path to the output diparity map mask: path to the output rejection mask algo: string used to indicate the desired binary. Currently it can be one among 'hirschmuller02', 'hirschmuller08', 'hirschmuller08_laplacian', 'hirschmuller08_cauchy', 'sgbm', 'msmw', 'tvl1', 'mgm', 'mgm_multi' and 'micmac' disp_min : smallest disparity to consider disp_max : biggest disparity to consider extra_params: optional string with algorithm-dependent parameters """ if rectify_secondary_tile_only(algo) is False: disp_min = [disp_min] disp_max = [disp_max] # limit disparity bounds np.alltrue(len(disp_min) == len(disp_max)) for dim in range(len(disp_min)): if disp_min[dim] is not None and disp_max[dim] is not None: image_size = common.image_size_gdal(im1) if disp_max[dim] - disp_min[dim] > image_size[dim]: center = 0.5 * (disp_min[dim] + disp_max[dim]) disp_min[dim] = int(center - 0.5 * image_size[dim]) disp_max[dim] = int(center + 0.5 * image_size[dim]) # round disparity bounds if disp_min[dim] is not None: disp_min[dim] = int(np.floor(disp_min[dim])) if disp_max is not None: disp_max[dim] = int(np.ceil(disp_max[dim])) if rectify_secondary_tile_only(algo) is False: disp_min = disp_min[0] disp_max = disp_max[0] # define environment variables env = os.environ.copy() env['OMP_NUM_THREADS'] = str(cfg['omp_num_threads']) # call the block_matching binary if algo == 'hirschmuller02': bm_binary = 'subpix.sh' common.run('{0} {1} {2} {3} {4} {5} {6} {7}'.format( bm_binary, im1, im2, disp, mask, disp_min, disp_max, extra_params)) # extra_params: LoG(0) regionRadius(3) # LoG: Laplacian of Gaussian preprocess 1:enabled 0:disabled # regionRadius: radius of the window if algo == 'hirschmuller08': bm_binary = 'callSGBM.sh' common.run('{0} {1} {2} {3} {4} {5} {6} {7}'.format( bm_binary, im1, im2, disp, mask, disp_min, disp_max, extra_params)) # extra_params: regionRadius(3) P1(default) P2(default) LRdiff(1) # regionRadius: radius of the window # P1, P2 : regularization parameters # LRdiff: maximum difference between left and right disparity maps if algo == 'hirschmuller08_laplacian': bm_binary = 'callSGBM_lap.sh' common.run('{0} {1} {2} {3} {4} {5} {6} {7}'.format( bm_binary, im1, im2, disp, mask, disp_min, disp_max, extra_params)) if algo == 'hirschmuller08_cauchy': bm_binary = 'callSGBM_cauchy.sh' common.run('{0} {1} {2} {3} {4} {5} {6} {7}'.format( bm_binary, im1, im2, disp, mask, disp_min, disp_max, extra_params)) if algo == 'sgbm': # opencv sgbm function implements a modified version of Hirschmuller's # Semi-Global Matching (SGM) algorithm described in "Stereo Processing # by Semiglobal Matching and Mutual Information", PAMI, 2008 p1 = 8 # penalizes disparity changes of 1 between neighbor pixels p2 = 32 # penalizes disparity changes of more than 1 # it is required that p2 > p1. The larger p1, p2, the smoother the disparity win = 3 # matched block size. It must be a positive odd number lr = 1 # maximum difference allowed in the left-right disparity check cost = common.tmpfile('.tif') common.run('sgbm {} {} {} {} {} {} {} {} {} {}'.format( im1, im2, disp, cost, disp_min, disp_max, win, p1, p2, lr)) # create rejection mask (0 means rejected, 1 means accepted) # keep only the points that are matched and present in both input images common.run( 'plambda {0} "x 0 join" | backflow - {2} | plambda {0} {1} - "x isfinite y isfinite z isfinite and and" -o {3}' .format(disp, im1, im2, mask)) if algo == 'tvl1': tvl1 = 'callTVL1.sh' common.run('{0} {1} {2} {3} {4}'.format(tvl1, im1, im2, disp, mask), env) if algo == 'tvl1_2d': tvl1 = 'callTVL1.sh' common.run( '{0} {1} {2} {3} {4} {5}'.format(tvl1, im1, im2, disp, mask, 1), env) if algo == 'msmw': bm_binary = 'iip_stereo_correlation_multi_win2' common.run( '{0} -i 1 -n 4 -p 4 -W 5 -x 9 -y 9 -r 1 -d 1 -t -1 -s 0 -b 0 -o 0.25 -f 0 -P 32 -m {1} -M {2} {3} {4} {5} {6}' .format(bm_binary, disp_min, disp_max, im1, im2, disp, mask)) if algo == 'msmw2': bm_binary = 'iip_stereo_correlation_multi_win2_newversion' common.run( '{0} -i 1 -n 4 -p 4 -W 5 -x 9 -y 9 -r 1 -d 1 -t -1 -s 0 -b 0 -o -0.25 -f 0 -P 32 -D 0 -O 25 -c 0 -m {1} -M {2} {3} {4} {5} {6}' .format(bm_binary, disp_min, disp_max, im1, im2, disp, mask)) if algo == 'msmw3': bm_binary = 'msmw' common.run('{0} -m {1} -M {2} -il {3} -ir {4} -dl {5} -kl {6}'.format( bm_binary, disp_min, disp_max, im1, im2, disp, mask)) if algo == 'mgm': env['MEDIAN'] = '1' env['CENSUS_NCC_WIN'] = str(cfg['census_ncc_win']) env['TSGM'] = '3' common.run( '{0} -r {1} -R {2} -s vfit -t census -O 8 {3} {4} {5}'.format( 'mgm', disp_min, disp_max, im1, im2, disp), env) # produce the mask: rejected pixels are marked with nan of inf in disp # map common.run('plambda {0} "isfinite" -o {1}'.format(disp, mask)) if algo == 'mgm_multi': env['REMOVESMALLCC'] = '25' env['MINDIFF'] = '1' env['CENSUS_NCC_WIN'] = str(cfg['census_ncc_win']) env['SUBPIX'] = '2' common.run( '{0} -r {1} -R {2} -S 3 -s vfit -t census {3} {4} {5}'.format( 'mgm_multi', disp_min, disp_max, im1, im2, disp), env) # produce the mask: rejected pixels are marked with nan of inf in disp # map common.run('plambda {0} "isfinite" -o {1}'.format(disp, mask)) if (algo == 'micmac'): # add micmac binaries to the PATH environment variable s2p_dir = os.path.dirname( os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) micmac_bin = os.path.join(s2p_dir, 'bin', 'micmac', 'bin') os.environ['PATH'] = os.environ['PATH'] + os.pathsep + micmac_bin # prepare micmac xml params file micmac_params = os.path.join(s2p_dir, '3rdparty', 'micmac_params.xml') work_dir = os.path.dirname(os.path.abspath(im1)) common.run('cp {0} {1}'.format(micmac_params, work_dir)) # run MICMAC common.run('MICMAC {0:s}'.format( os.path.join(work_dir, 'micmac_params.xml'))) # copy output disp map micmac_disp = os.path.join(work_dir, 'MEC-EPI', 'Px1_Num6_DeZoom1_LeChantier.tif') disp = os.path.join(work_dir, 'rectified_disp.tif') common.run('cp {0} {1}'.format(micmac_disp, disp)) # compute mask by rejecting the 10% of pixels with lowest correlation score micmac_cost = os.path.join(work_dir, 'MEC-EPI', 'Correl_LeChantier_Num_5.tif') mask = os.path.join(work_dir, 'rectified_mask.png') common.run('plambda {0} "x x%q10 < 0 255 if" -o {1}'.format( micmac_cost, mask))
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 multidisparities_to_ply(tile): """ Compute a point cloud from the disparity maps of N-pairs of image tiles. Args: tile: dictionary containing the information needed to process a tile. # There is no guarantee that this function works with z!=1 """ 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'] rpc_ref = cfg['images'][0]['rpc'] disp_list = list() rpc_list = list() if cfg['skip_existing'] and os.path.isfile(ply_file): print('triangulation done on tile {} {}'.format(x, y)) return mask_orig = os.path.join(out_dir, 'cloud_water_image_domain_mask.png') print('triangulating tile {} {}...'.format(x, y)) n = len(cfg['images']) - 1 for i in range(n): pair = 'pair_%d' % (i + 1) H_ref = os.path.join(out_dir, pair, 'H_ref.txt') H_sec = os.path.join(out_dir, pair, 'H_sec.txt') disp = os.path.join(out_dir, pair, 'rectified_disp.tif') mask_rect = os.path.join(out_dir, pair, 'rectified_mask.png') disp2D = os.path.join(out_dir, pair, 'disp2D.tif') rpc_sec = cfg['images'][i + 1]['rpc'] if os.path.exists(disp): # homography for warp T = common.matrix_translation(x, y) hom_ref = np.loadtxt(H_ref) hom_ref_shift = np.dot(hom_ref, T) # homography for 1D to 2D conversion hom_sec = np.loadtxt(H_sec) if cfg["use_global_pointing_for_geometric_triangulation"] is True: pointing = os.path.join(cfg['out_dir'], 'global_pointing_%s.txt' % pair) hom_pointing = np.loadtxt(pointing) hom_sec = np.dot(hom_sec, np.linalg.inv(hom_pointing)) hom_sec_shift_inv = np.linalg.inv(hom_sec) h1 = " ".join(str(x) for x in hom_ref_shift.flatten()) h2 = " ".join(str(x) for x in hom_sec_shift_inv.flatten()) # relative disparity map to absolute disparity map tmp_abs = common.tmpfile('.tif') os.environ["PLAMBDA_GETPIXEL"] = "0" common.run( 'plambda %s %s "y 0 = nan x[0] :i + x[1] :j + 1 3 njoin if" -o %s' % (disp, mask_rect, tmp_abs)) # 1d to 2d conversion tmp_1d_to_2d = common.tmpfile('.tif') common.run('plambda %s "%s 9 njoin x mprod" -o %s' % (tmp_abs, h2, tmp_1d_to_2d)) # warp tmp_warp = common.tmpfile('.tif') common.run('homwarp -o 2 "%s" %d %d %s %s' % (h1, w, h, tmp_1d_to_2d, tmp_warp)) # set masked value to NaN exp = 'y 0 = nan x if' common.run('plambda %s %s "%s" -o %s' % (tmp_warp, mask_orig, exp, disp2D)) # disp2D contains positions in the secondary image # added input data for triangulation module disp_list.append(disp2D) rpc_list.append(rpc_sec) if cfg['clean_intermediate']: common.remove(H_ref) common.remove(H_sec) common.remove(disp) common.remove(mask_rect) common.remove(mask_orig) colors = os.path.join(out_dir, 'ref.png') if cfg['images'][0]['clr']: common.image_crop_gdal(cfg['images'][0]['clr'], x, y, w, h, colors) else: common.image_qauto( common.image_crop_gdal(cfg['images'][0]['img'], x, y, w, h), colors) # compute the point cloud triangulation.multidisp_map_to_point_cloud(ply_file, disp_list, rpc_ref, rpc_list, colors, utm_zone=cfg['utm_zone'], llbbx=tuple(cfg['ll_bbx']), xybbx=(x, x + w, y, y + h)) # compute the point cloud extrema (xmin, xmax, xmin, ymax) common.run("plyextrema %s %s" % (ply_file, plyextrema)) if cfg['clean_intermediate']: common.remove(colors)
def cloud_water_image_domain(x, y, w, h, rpc, roi_gml=None, cld_gml=None, wat_msk=None, use_srtm_for_water=False): """ Compute a mask for pixels masked by clouds, water, or out of image domain. Args: x, y, w, h: coordinates of the ROI rpc: path to the xml file containing the rpc coefficients of the image RPC model is used with SRTM data to derive the water mask roi_gml (optional): path to a gml file containing a mask defining the area contained in the full image cld_gml (optional): path to a gml file containing a mask defining the areas covered by clouds wat_msk (optional): path to an image file containing a water mask Returns: 2D array containing the output binary mask. 0 indicate masked pixels, 1 visible pixels. """ # coefficients of the transformation associated to the crop and zoom z = cfg['subsampling_factor'] H = np.dot(np.diag((1 / z, 1 / z, 1)), common.matrix_translation(-x, -y)) hij = ' '.join([str(x) for x in H.flatten()]) w, h = int(w / z), int(h / z) mask = np.ones((h, w), dtype=np.bool) if roi_gml is not None: # image domain mask (polygons) tmp = common.tmpfile('.png') subprocess.check_call('cldmask %d %d -h "%s" %s %s' % (w, h, hij, roi_gml, tmp), shell=True) f = gdal.Open(tmp) mask = np.logical_and(mask, f.ReadAsArray()) f = None # this is the gdal way of closing files if not mask.any(): return mask if cld_gml is not None: # cloud mask (polygons) tmp = common.tmpfile('.png') subprocess.check_call('cldmask %d %d -h "%s" %s %s' % (w, h, hij, cld_gml, tmp), shell=True) f = gdal.Open(tmp) mask = np.logical_and(mask, ~f.ReadAsArray().astype(bool)) f = None # this is the gdal way of closing files if not mask.any(): return mask if wat_msk is not None: # water mask (raster) f = gdal.Open(wat_msk) mask = np.logical_and(mask, f.ReadAsArray(x, y, w, h)) f = None # this is the gdal way of closing files elif use_srtm_for_water: # water mask (srtm) tmp = common.tmpfile('.png') env = os.environ.copy() env['SRTM4_CACHE'] = cfg['srtm_dir'] subprocess.check_call('watermask %d %d -h "%s" %s %s' % (w, h, hij, rpc, tmp), shell=True, env=env) f = gdal.Open(tmp) mask = np.logical_and(mask, f.ReadAsArray()) f = None # this is the gdal way of closing files return mask
def compute_disparity_map(im1, im2, disp, mask, algo, disp_min=None, disp_max=None, extra_params=''): """ Runs a block-matching binary on a pair of stereo-rectified images. Args: im1, im2: rectified stereo pair disp: path to the output diparity map mask: path to the output rejection mask algo: string used to indicate the desired binary. Currently it can be one among 'hirschmuller02', 'hirschmuller08', 'hirschmuller08_laplacian', 'hirschmuller08_cauchy', 'sgbm', 'msmw', 'tvl1', 'mgm', 'mgm_multi' and 'micmac' disp_min : smallest disparity to consider disp_max : biggest disparity to consider extra_params: optional string with algorithm-dependent parameters """ if rectify_secondary_tile_only(algo) is False: disp_min = [disp_min] disp_max = [disp_max] # limit disparity bounds np.alltrue(len(disp_min) == len(disp_max)) for dim in range(len(disp_min)): if disp_min[dim] is not None and disp_max[dim] is not None: image_size = common.image_size_gdal(im1) if disp_max[dim] - disp_min[dim] > image_size[dim]: center = 0.5 * (disp_min[dim] + disp_max[dim]) disp_min[dim] = int(center - 0.5 * image_size[dim]) disp_max[dim] = int(center + 0.5 * image_size[dim]) # round disparity bounds if disp_min[dim] is not None: disp_min[dim] = int(np.floor(disp_min[dim])) if disp_max is not None: disp_max[dim] = int(np.ceil(disp_max[dim])) if rectify_secondary_tile_only(algo) is False: disp_min = disp_min[0] disp_max = disp_max[0] # define environment variables env = os.environ.copy() env['OMP_NUM_THREADS'] = str(cfg['omp_num_threads']) # call the block_matching binary if algo == 'hirschmuller02': bm_binary = 'subpix.sh' common.run('{0} {1} {2} {3} {4} {5} {6} {7}'.format(bm_binary, im1, im2, disp, mask, disp_min, disp_max, extra_params)) # extra_params: LoG(0) regionRadius(3) # LoG: Laplacian of Gaussian preprocess 1:enabled 0:disabled # regionRadius: radius of the window if algo == 'hirschmuller08': bm_binary = 'callSGBM.sh' common.run('{0} {1} {2} {3} {4} {5} {6} {7}'.format(bm_binary, im1, im2, disp, mask, disp_min, disp_max, extra_params)) # extra_params: regionRadius(3) P1(default) P2(default) LRdiff(1) # regionRadius: radius of the window # P1, P2 : regularization parameters # LRdiff: maximum difference between left and right disparity maps if algo == 'hirschmuller08_laplacian': bm_binary = 'callSGBM_lap.sh' common.run('{0} {1} {2} {3} {4} {5} {6} {7}'.format(bm_binary, im1, im2, disp, mask, disp_min, disp_max, extra_params)) if algo == 'hirschmuller08_cauchy': bm_binary = 'callSGBM_cauchy.sh' common.run('{0} {1} {2} {3} {4} {5} {6} {7}'.format(bm_binary, im1, im2, disp, mask, disp_min, disp_max, extra_params)) if algo == 'sgbm': # opencv sgbm function implements a modified version of Hirschmuller's # Semi-Global Matching (SGM) algorithm described in "Stereo Processing # by Semiglobal Matching and Mutual Information", PAMI, 2008 p1 = 8 # penalizes disparity changes of 1 between neighbor pixels p2 = 32 # penalizes disparity changes of more than 1 # it is required that p2 > p1. The larger p1, p2, the smoother the disparity win = 3 # matched block size. It must be a positive odd number lr = 1 # maximum difference allowed in the left-right disparity check cost = common.tmpfile('.tif') common.run('sgbm {} {} {} {} {} {} {} {} {} {}'.format(im1, im2, disp, cost, disp_min, disp_max, win, p1, p2, lr)) # create rejection mask (0 means rejected, 1 means accepted) # keep only the points that are matched and present in both input images common.run('plambda {0} "x 0 join" | backflow - {2} | plambda {0} {1} - "x isfinite y isfinite z isfinite and and" -o {3}'.format(disp, im1, im2, mask)) if algo == 'tvl1': tvl1 = 'callTVL1.sh' common.run('{0} {1} {2} {3} {4}'.format(tvl1, im1, im2, disp, mask), env) if algo == 'tvl1_2d': tvl1 = 'callTVL1.sh' common.run('{0} {1} {2} {3} {4} {5}'.format(tvl1, im1, im2, disp, mask, 1), env) if algo == 'msmw': bm_binary = 'iip_stereo_correlation_multi_win2' common.run('{0} -i 1 -n 4 -p 4 -W 5 -x 9 -y 9 -r 1 -d 1 -t -1 -s 0 -b 0 -o 0.25 -f 0 -P 32 -m {1} -M {2} {3} {4} {5} {6}'.format(bm_binary, disp_min, disp_max, im1, im2, disp, mask)) if algo == 'msmw2': bm_binary = 'iip_stereo_correlation_multi_win2_newversion' common.run('{0} -i 1 -n 4 -p 4 -W 5 -x 9 -y 9 -r 1 -d 1 -t -1 -s 0 -b 0 -o -0.25 -f 0 -P 32 -D 0 -O 25 -c 0 -m {1} -M {2} {3} {4} {5} {6}'.format( bm_binary, disp_min, disp_max, im1, im2, disp, mask)) if algo == 'msmw3': bm_binary = 'msmw' common.run('{0} -m {1} -M {2} -il {3} -ir {4} -dl {5} -kl {6}'.format( bm_binary, disp_min, disp_max, im1, im2, disp, mask)) if algo == 'mgm': env['MEDIAN'] = '1' env['CENSUS_NCC_WIN'] = str(cfg['census_ncc_win']) env['TSGM'] = '3' common.run('{0} -r {1} -R {2} -s vfit -t census -O 8 {3} {4} {5}'.format('mgm', disp_min, disp_max, im1, im2, disp), env) # produce the mask: rejected pixels are marked with nan of inf in disp # map common.run('plambda {0} "isfinite" -o {1}'.format(disp, mask)) if (algo == 'micmac'): # add micmac binaries to the PATH environment variable s2p_dir = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) micmac_bin = os.path.join(s2p_dir, 'bin', 'micmac', 'bin') os.environ['PATH'] = os.environ['PATH'] + os.pathsep + micmac_bin # prepare micmac xml params file micmac_params = os.path.join(s2p_dir, '3rdparty', 'micmac_params.xml') work_dir = os.path.dirname(os.path.abspath(im1)) common.run('cp {0} {1}'.format(micmac_params, work_dir)) # run MICMAC common.run('MICMAC {0:s}'.format(os.path.join(work_dir, 'micmac_params.xml'))) # copy output disp map micmac_disp = os.path.join(work_dir, 'MEC-EPI', 'Px1_Num6_DeZoom1_LeChantier.tif') disp = os.path.join(work_dir, 'rectified_disp.tif') common.run('cp {0} {1}'.format(micmac_disp, disp)) # compute mask by rejecting the 10% of pixels with lowest correlation score micmac_cost = os.path.join(work_dir, 'MEC-EPI', 'Correl_LeChantier_Num_5.tif') mask = os.path.join(work_dir, 'rectified_mask.png') common.run('plambda {0} "x x%q10 < 0 255 if" -o {1}'.format(micmac_cost, mask))
def multidisparities_to_ply(tile): """ Compute a point cloud from the disparity maps of N-pairs of image tiles. Args: tile: dictionary containing the information needed to process a tile. # There is no guarantee that this function works with z!=1 """ 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'] rpc_ref = cfg['images'][0]['rpc'] disp_list = list() rpc_list = list() if cfg['skip_existing'] and os.path.isfile(ply_file): print('triangulation done on tile {} {}'.format(x, y)) return mask_orig = os.path.join(out_dir, 'cloud_water_image_domain_mask.png') print('triangulating tile {} {}...'.format(x, y)) n = len(cfg['images']) - 1 for i in range(n): pair = 'pair_%d' % (i+1) H_ref = os.path.join(out_dir, pair, 'H_ref.txt') H_sec = os.path.join(out_dir, pair, 'H_sec.txt') disp = os.path.join(out_dir, pair, 'rectified_disp.tif') mask_rect = os.path.join(out_dir, pair, 'rectified_mask.png') disp2D = os.path.join(out_dir, pair, 'disp2D.tif') rpc_sec = cfg['images'][i+1]['rpc'] if os.path.exists(disp): # homography for warp T = common.matrix_translation(x, y) hom_ref = np.loadtxt(H_ref) hom_ref_shift = np.dot(hom_ref, T) # homography for 1D to 2D conversion hom_sec = np.loadtxt(H_sec) if cfg["use_global_pointing_for_geometric_triangulation"] is True: pointing = os.path.join(cfg['out_dir'], 'global_pointing_%s.txt' % pair) hom_pointing = np.loadtxt(pointing) hom_sec = np.dot(hom_sec,np.linalg.inv(hom_pointing)) hom_sec_shift_inv = np.linalg.inv(hom_sec) h1 = " ".join(str(x) for x in hom_ref_shift.flatten()) h2 = " ".join(str(x) for x in hom_sec_shift_inv.flatten()) # relative disparity map to absolute disparity map tmp_abs = common.tmpfile('.tif') os.environ["PLAMBDA_GETPIXEL"] = "0" common.run('plambda %s %s "y 0 = nan x[0] :i + x[1] :j + 1 3 njoin if" -o %s' % (disp, mask_rect, tmp_abs)) # 1d to 2d conversion tmp_1d_to_2d = common.tmpfile('.tif') common.run('plambda %s "%s 9 njoin x mprod" -o %s' % (tmp_abs, h2, tmp_1d_to_2d)) # warp tmp_warp = common.tmpfile('.tif') common.run('homwarp -o 2 "%s" %d %d %s %s' % (h1, w, h, tmp_1d_to_2d, tmp_warp)) # set masked value to NaN exp = 'y 0 = nan x if' common.run('plambda %s %s "%s" -o %s' % (tmp_warp, mask_orig, exp, disp2D)) # disp2D contains positions in the secondary image # added input data for triangulation module disp_list.append(disp2D) rpc_list.append(rpc_sec) if cfg['clean_intermediate']: common.remove(H_ref) common.remove(H_sec) common.remove(disp) common.remove(mask_rect) common.remove(mask_orig) colors = os.path.join(out_dir, 'ref.png') if cfg['images'][0]['clr']: common.image_crop_gdal(cfg['images'][0]['clr'], x, y, w, h, colors) else: common.image_qauto(common.image_crop_gdal(cfg['images'][0]['img'], x, y, w, h), colors) # compute the point cloud triangulation.multidisp_map_to_point_cloud(ply_file, disp_list, rpc_ref, rpc_list, colors, utm_zone=cfg['utm_zone'], llbbx=tuple(cfg['ll_bbx']), xybbx=(x, x+w, y, y+h)) # compute the point cloud extrema (xmin, xmax, xmin, ymax) common.run("plyextrema %s %s" % (ply_file, plyextrema)) if cfg['clean_intermediate']: common.remove(colors)
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 cloud_water_image_domain(x, y, w, h, rpc, roi_gml=None, cld_gml=None, wat_msk=None, use_srtm_for_water=False): """ Compute a mask for pixels masked by clouds, water, or out of image domain. Args: x, y, w, h: coordinates of the ROI rpc: path to the xml file containing the rpc coefficients of the image RPC model is used with SRTM data to derive the water mask roi_gml (optional): path to a gml file containing a mask defining the area contained in the full image cld_gml (optional): path to a gml file containing a mask defining the areas covered by clouds wat_msk (optional): path to an image file containing a water mask Returns: 2D array containing the output binary mask. 0 indicate masked pixels, 1 visible pixels. """ # coefficients of the transformation associated to the crop and zoom z = cfg['subsampling_factor'] H = np.dot(np.diag((1/z, 1/z, 1)), common.matrix_translation(-x, -y)) hij = ' '.join([str(el) for el in H.flatten()]) w, h = int(w/z), int(h/z) mask = np.ones((h, w), dtype=np.bool) if roi_gml is not None: # image domain mask (polygons) tmp = common.tmpfile('.png') subprocess.check_call('cldmask %d %d -h "%s" %s %s' % (w, h, hij, roi_gml, tmp), shell=True) f = gdal.Open(tmp) mask = np.logical_and(mask, f.ReadAsArray()) f = None # this is the gdal way of closing files if not mask.any(): return mask if cld_gml is not None: # cloud mask (polygons) tmp = common.tmpfile('.png') subprocess.check_call('cldmask %d %d -h "%s" %s %s' % (w, h, hij, cld_gml, tmp), shell=True) f = gdal.Open(tmp) mask = np.logical_and(mask, ~f.ReadAsArray().astype(bool)) f = None # this is the gdal way of closing files if not mask.any(): return mask if wat_msk is not None: # water mask (raster) f = gdal.Open(wat_msk) mask = np.logical_and(mask, f.ReadAsArray(x, y, w, h)) f = None # this is the gdal way of closing files elif use_srtm_for_water: # water mask (srtm) tmp = common.tmpfile('.png') env = os.environ.copy() env['SRTM4_CACHE'] = cfg['srtm_dir'] subprocess.check_call('watermask %d %d -h "%s" %s %s' % (w, h, hij, rpc, tmp), shell=True, env=env) f = gdal.Open(tmp) mask = np.logical_and(mask, f.ReadAsArray()) f = None # this is the gdal way of closing files return mask