def main(argsIn): try: usage = '''usage: process_icebridge_run.py <image_folder> <camera_folder> <lidar_folder> <output_folder>''' parser = optparse.OptionParser(usage=usage) # Data selection optios parser.add_option('--start-frame', dest='startFrame', default=-1, type='int', help='The frame number to start processing with.') parser.add_option('--stop-frame', dest='stopFrame', default=-1, type='int', help='The frame number to finish processing with.') parser.add_option('--south', action='store_true', default=False, dest='isSouth', help='MUST be set if the images are in the southern hemisphere.') # Processing options parser.add_option('--stereo-arguments', dest='stereoArgs', default='', help='Additional argument string to be passed to the stereo command.') parser.add_option('--bundle-length', dest='bundleLength', default=2, type='int', help='Number of images to bundle adjust and process at once.') parser.add_option('--image-stereo-interval', dest='imageStereoInterval', default=None, type='int', help='Advance this many frames to get the stereo pair. Default is auto-calculate') parser.add_option('--solve-intrinsics', action='store_true', default=False, dest='solve_intr', help='If to float the intrinsics params.') #parser.add_option('--dem-resolution', dest='demResolution', default=0.4, # type='float', help='Generate output DEMs at this resolution.') parser.add_option('--max-displacement', dest='maxDisplacement', default=20, type='float', help='Max displacement value passed to pc_align.') # Performance options parser.add_option('--num-processes', dest='numProcesses', default=1, type='int', help='The number of simultaneous processes to run.') parser.add_option('--num-threads', dest='numThreads', default=None, type='int', help='The number threads to use per process.') # Action options parser.add_option('--interactive', action='store_true', default=False, dest='interactive', help='If to wait on user input to terminate the jobs.') parser.add_option('--log-batches', action='store_true', default=False, dest='logBatches', help="Just log the batch commands to a file.") parser.add_option('--cleanup', action='store_true', default=False, dest='cleanup', help='If the final result is produced delete intermediate files.') parser.add_option('--many-ip', action='store_true', default=False, dest='manyip', help='If to use a lot of IP in bundle adjustment from the beginning.') parser.add_option('--dry-run', action='store_true', default=False, dest='dryRun', help="Print but don't launch the processing jobs.") parser.add_option('--ortho-folder', dest='orthoFolder', default=None, help='Use ortho files to adjust processing to the image spacing.') parser.add_option('--fireball-folder', dest='fireballFolder', default=None, help='Location of fireball DEMs for comparison.') parser.add_option('--reference-dem', dest='referenceDem', default=None, help='Reference DEM used to calculate the expected GSD.') (options, args) = parser.parse_args(argsIn) if len(args) < 4: print(usage) return 0 imageFolder = args[0] cameraFolder = args[1] lidarFolder = args[2] outputFolder = args[3] except optparse.OptionError as msg: raise Usage(msg) os.system("ulimit -c 0") # disable core dumps os.system("umask 022") # enforce files be readable by others # Check the inputs for f in [imageFolder, cameraFolder, lidarFolder]: if not os.path.exists(f): logger.error('Input folder '+ f +' does not exist!') return 0 asp_system_utils.mkdir_p(outputFolder) suppressOutput = False redo = False logger.info('\nStarting processing...') # Get a list of all the input files imageCameraPairs = icebridge_common.getImageCameraPairs(imageFolder, cameraFolder, options.startFrame, options.stopFrame, logger) numFiles = len(list(imageCameraPairs)) if numFiles < 2: raise Exception('Failed to find any image camera pairs!') # Check that the files are properly aligned lastFrame = -1 availableFrames = [] for (image, camera) in imageCameraPairs: frameNumber = icebridge_common.getFrameNumberFromFilename(image) availableFrames.append(frameNumber) if (icebridge_common.getFrameNumberFromFilename(camera) != frameNumber): logger.error('Error: input files do not align!\n' + str((image, camera))) return -1 if frameNumber <= lastFrame: logger.error('Error: input frames not sorted properly!\n') return -1 lastFrame = frameNumber # Do not compute output resolution. Will be overwritten anyway per frame. ## Set the output resolution as the computed mean GSD ## TODO: This should be cashed, and recomputed only when the batch file changes! #NUM_GSD_FRAMES = 20 #logger.info('Computing GSD with ' + str(NUM_GSD_FRAMES) + ' frames.') #gsdFrameSkip = len(imageCameraPairs) / NUM_GSD_FRAMES #if gsdFrameSkip < 1: # gsdFrameSkip = 1 #medianGsd = getRunMedianGsd(imageCameraPairs, options.referenceDem, options.isSouth, # gsdFrameSkip) #outputResolution = icebridge_common.gsdToDemRes(medianGsd) #logger.info('OUTPUT_RESOLUTION: ' + str(outputResolution)) outputResolution = 0.4 # Generate a map of initial camera positions orbitvizBefore = os.path.join(outputFolder, 'cameras_in.kml') vizString = '' for (image, camera) in imageCameraPairs: vizString += camera+' ' cmd = 'orbitviz_pinhole --hide-labels -o '+ orbitvizBefore +' '+ vizString logger.info('Running orbitviz on input files...') # Suppress (potentially long) output asp_system_utils.executeCommand(cmd, orbitvizBefore, True, redo) # Set up options for process_icebridge_batch extraOptions = '' if options.numThreads: extraOptions += ' --num-threads ' + str(options.numThreads) if options.solve_intr: extraOptions += ' --solve-intrinsics ' if options.isSouth: extraOptions += ' --south ' if options.maxDisplacement: extraOptions += ' --max-displacement ' + str(options.maxDisplacement) if options.fireballFolder: extraOptions += ' --fireball-folder ' + str(options.fireballFolder) if options.cleanup: extraOptions += ' --cleanup ' if options.manyip: extraOptions += ' --many-ip ' # We ran this before, as part of fetching, so hopefully all the data is cached forceAllFramesInRange = False (breaks, largeSkips) = getImageSpacing(options.orthoFolder, availableFrames, options.startFrame, options.stopFrame, forceAllFramesInRange) if options.imageStereoInterval: logger.info('Using manually specified image stereo interval: ' + str(options.imageStereoInterval)) largeSkips = [] # Always use a manually specified skip interval else: options.imageStereoInterval = 1 sleepTime = 20 # If all we are doing is logging commands then one process is sufficient. # - Wipe the output file while we are at it. batchLogPath = '' batchNum = 0 if options.logBatches: options.numProcesses = 1 sleepTime = 1 batchLogPath = os.path.join(outputFolder, BATCH_COMMAND_LOG_FILE) os.system('rm -f ' + batchLogPath) logger.info('Just generating batch log file '+batchLogPath+', no processing will occur.') logger.info('Starting processing pool with ' + str(options.numProcesses) +' processes.') pool = NonDaemonPool(options.numProcesses) # Call process_icebridge_batch on each batch of images. # - Batch size should be the largest number of images which can be effectively bundle-adjusted. taskHandles = [] batchImageCameraPairs = [] frameNumbers = [] i = 0 # The frame index that starts the current batch while True: # Loop for adding batches # Bugfix: arrived at the last feasible frame (maybe there are more but # they lack cameras). if i >= len(list(imageCameraPairs)): break firstBundleFrame = icebridge_common.getFrameNumberFromFilename(imageCameraPairs[i][0]) # Determine the frame skip amount for this batch (set by the first frame) thisSkipInterval = options.imageStereoInterval if firstBundleFrame in largeSkips: #print(" ", firstBundleFrame, " in largeskips!" ) thisSkipInterval = largeSkips[firstBundleFrame] thisBatchSize = options.bundleLength + thisSkipInterval - 1 # Keep adding frames until we have enough or run out of frames j = i # The frame index that ends the batch while True: if j >= len(imageCameraPairs): # Bugfix: arrived at the last feasible frame. break frameNumber = icebridge_common.getFrameNumberFromFilename(imageCameraPairs[j][0]) # Useful debugging code #print("Framenumber is ", frameNumber) #if int(frameNumber) > 8531: # # pass # for t in range(8340, 8360): # print("i frame is ", t, imageCameraPairs[t][0]) # print("breaks are ", breaks) # sys.exit(1) # Update conditions hitBreakFrame = (frameNumber in breaks) lastFrame = (frameNumber > options.stopFrame) or (j >= numFiles) endBatch = ( len(frameNumbers) >= thisBatchSize ) if lastFrame or endBatch: break # The new frame is too much, don't add it to the batch # Add frame to the list for the current batch batchImageCameraPairs.append(imageCameraPairs[j]) frameNumbers.append(frameNumber) if hitBreakFrame: logger.info("Hit a break, won't start a batch with frame: " + str(frameNumber)) break # Break after this frame, it is the last one added to the batch. j = j + 1 # Done adding frames to this batch if len(frameNumbers) <= thisSkipInterval: logger.info('Batch from frame: ' + str(firstBundleFrame) + ' is too small to run. Skipping.') else: # Submit the batch if not options.logBatches: logger.info('Processing frame number: ' + str(firstBundleFrame)) # The output folder is named after the first and last frame in the batch. # We count on this convention in blend_dems.py. batchFolderName = icebridge_common.batchFolderName(frameNumbers[0], frameNumbers[-1], options.bundleLength) thisOutputFolder = os.path.join(outputFolder, batchFolderName) if not options.logBatches: logger.info('Running processing batch in output folder: ' + \ thisOutputFolder + '\n' + 'with options: ' + \ extraOptions + ' --stereo-arguments ' + \ options.stereoArgs) if not options.dryRun: # Generate the command call taskHandles.append(pool.apply_async(processBatch, (batchImageCameraPairs, lidarFolder, options.referenceDem, thisSkipInterval, thisOutputFolder, extraOptions, outputResolution, options.stereoArgs, batchNum, batchLogPath))) batchNum += 1 # Reset these lists batchImageCameraPairs = [] frameNumbers = [] # Advance to the frame that starts the next batch if hitBreakFrame: # When we hit a break in the frames we need to start the # next batch after the break frame #print("Hit break frame for i, j, frameNumber", i, j, frameNumber) i = j + 1 else: # Start in the next frame that was not used as a "left" stereo image. i = i + options.bundleLength - 1 if lastFrame: break # Quit the main loop if we hit the end of the frame list. # End of loop through input file pairs logger.info('Finished adding ' + str(len(taskHandles)) + ' tasks to the pool.') # Wait for all the tasks to complete icebridge_common.waitForTaskCompletionOrKeypress(taskHandles, logger, options.interactive, quitKey='q', sleepTime=sleepTime) # Either all the tasks are finished or the user requested a cancel. # Clean up the processing pool icebridge_common.stopTaskPool(pool) logger.info('Finished process_icebridge_run.') # to avoid ending a log with 'waiting ...'
def solveIntrinsics_Part2(options, imageFolder, cameraFolder, lidarFolder, orthoFolder, processedFolder, isSouth, logger): '''Create a camera model with optimized intrinsics. By now we processed a bunch of images and created bundle-adjusted and pc_aligned cameras and DEMs while using a camera model with distortion implemented using RPC coefficients which was obtained from the photometrics model. We now use the obtained cameras as inputs to a bundle adjust problem where we will optimize the intrinsics, including the distortion RPC coefficients, using the lidar as an external constraint, and many dense IP pairs and triplets (no quadruplets yet, even if 4 images overlap).''' # Get a list of all the input files imageCameraPairs = icebridge_common.getImageCameraPairs( imageFolder, cameraFolder, options.startFrame, options.stopFrame, logger) # The paired lidar file for the first image should be huge enough to contain # all images. lidarFile = icebridge_common.findMatchingLidarFile(imageCameraPairs[0][0], lidarFolder) logger.info('Found matching lidar file ' + lidarFile) lidarCsvFormatString = icebridge_common.getLidarCsvFormat(lidarFile) numFiles = len(imageCameraPairs) if numFiles < 2: raise Exception('Failed to find any image camera pairs!') if numFiles % 2 != 0: raise Exception( "When solving for intrinsics, must have an even number of frames to use." ) # Collect pc_align-ed cameras, unaligned disparities, and dense match files images = [] cameras = [] dispFiles = [] for it in range(numFiles / 2): begFrame = options.startFrame + 2 * it endFrame = begFrame + 1 batchFolderName = icebridge_common.batchFolderName( begFrame, endFrame, options.bundleLength) thisOutputFolder = os.path.join(processedFolder, batchFolderName) # Find all the cameras after bundle adjustment and pc_align. pattern = icebridge_common.getAlignedBundlePrefix( thisOutputFolder) + '*.tsai' alignedCameras = glob.glob(pattern) if len(alignedCameras) != options.bundleLength: raise Exception("Expected " + str(options.bundleLength) + " cameras, here's what " + " was obtained instead: " + " ".join(alignedCameras)) img0 = "" cam0 = "" img1 = "" cam1 = "" for cam in alignedCameras: frame = icebridge_common.getFrameNumberFromFilename(cam) if begFrame == frame: img0 = imageCameraPairs[2 * it][0] cam0 = cam if endFrame == frame: img1 = imageCameraPairs[2 * it + 1][0] cam1 = cam images.append(img0) images.append(img1) cameras.append(cam0) cameras.append(cam1) # Unaligned disparity stereoFolder = os.path.join(thisOutputFolder, 'stereo_pair_' + str(0)) currDispFiles = glob.glob( os.path.join(stereoFolder, '*unaligned-D.tif')) if len(currDispFiles) != 1: raise Exception("Expecting a single unaligned disparity file in " + stereoFolder) dispFiles.append(currDispFiles[0]) # Match files matchFiles = [] for it in range(numFiles - 1): begFrame = options.startFrame + it endFrame = begFrame + 1 batchFolderName = icebridge_common.batchFolderName( begFrame, endFrame, options.bundleLength) thisOutputFolder = os.path.join(processedFolder, batchFolderName) stereoFolder = os.path.join(thisOutputFolder, 'stereo_pair_' + str(0)) DISP_PREFIX = "disp-" currMatchFiles = glob.glob( os.path.join(stereoFolder, '*' + DISP_PREFIX + '*.match')) if len(currMatchFiles) != 1: raise Exception("Expecting a single dense match file in " + stereoFolder) matchFiles.append(currMatchFiles[0]) # Create output directory for bundle adjustment and copy there the match files baDir = os.path.join(processedFolder, "bundle_intrinsics") baPrefix = os.path.join(baDir, "out") os.system("mkdir -p " + baDir) for matchFile in matchFiles: dstFile = os.path.basename(matchFile) dstFile = dstFile.replace(DISP_PREFIX, '') dstFile = os.path.join(baDir, dstFile) cmd = "cp -f " + matchFile + " " + dstFile logger.info(cmd) os.system(cmd) # The bundle adjustment cmd = "bundle_adjust " + " ".join(images) + " " + " ".join(cameras) + \ ' --reference-terrain ' + lidarFile + \ ' --disparity-list "' + " ".join(dispFiles) + '"' + \ ' --datum wgs84 -t nadirpinhole --local-pinhole --robust-threshold 2' + \ ' --camera-weight 1 --solve-intrinsics --csv-format ' + lidarCsvFormatString + \ ' --overlap-limit 1 --max-disp-error 10 --max-iterations 100 ' + \ ' --parameter-tolerance 1e-12 -o ' + baPrefix logger.info(cmd) os.system(cmd) # Generate DEMs of residuals before and after optimization projString = icebridge_common.getEpsgCode(isSouth, asString=True) for val in ['initial', 'final']: cmd = 'point2dem --t_srs ' + projString + ' --tr 2' + \ ' --csv-format 1:lon,2:lat,4:height_above_datum' + \ ' ' + baPrefix + '-' + val + '_residuals_no_loss_function_pointmap_point_log.csv' logger.info(cmd) os.system(cmd) cmd = 'point2dem --t_srs ' + projString + ' --tr 2' + \ ' --csv-format 1:lon,2:lat,4:height_above_datum' + \ ' ' + baPrefix + '-' + val +'_residuals_no_loss_function_reference_terrain.txt' logger.info(cmd) os.system(cmd) # Look at the latest written tsai file, that will be the optimized distortion file. # Force the initial rotation and translation to be the identity, this is # expected by ortho2pinhole. outFiles = filter(os.path.isfile, glob.glob(baPrefix + '*.tsai')) outFiles.sort(key=lambda x: os.path.getmtime(x)) optFile = outFiles[-1] logger.info("Reading optimized file: " + optFile) with open(optFile, 'r') as f: lines = f.readlines() for it in range(len(lines)): lines[it] = lines[it].strip() if re.match("^C\s*=\s*", lines[it]): lines[it] = "C = 0 0 0" if re.match("^R\s*=\s*", lines[it]): lines[it] = "R = 1 0 0 0 1 0 0 0 1" # Write the final desired optimized RPC file logger.info("Writing final optimized file: " + options.outputCalCamera) os.system("mkdir -p " + os.path.dirname(options.outputCalCamera)) with open(options.outputCalCamera, 'w') as f: for line in lines: f.write(line + "\n")
def solveIntrinsics_Part2(options, imageFolder, cameraFolder, lidarFolder, orthoFolder, processedFolder, isSouth, logger): '''Create a camera model with optimized intrinsics. By now we processed a bunch of images and created bundle-adjusted and pc_aligned cameras and DEMs while using a camera model with distortion implemented using RPC coefficients which was obtained from the photometrics model. We now use the obtained cameras as inputs to a bundle adjust problem where we will optimize the intrinsics, including the distortion RPC coefficients, using the lidar as an external constraint, and many dense IP pairs and triplets (no quadruplets yet, even if 4 images overlap).''' # Get a list of all the input files imageCameraPairs = icebridge_common.getImageCameraPairs(imageFolder, cameraFolder, options.startFrame, options.stopFrame, logger) # The paired lidar file for the first image should be huge enough to contain # all images. lidarFile = icebridge_common.findMatchingLidarFile(imageCameraPairs[0][0], lidarFolder) logger.info('Found matching lidar file ' + lidarFile) lidarCsvFormatString = icebridge_common.getLidarCsvFormat(lidarFile) numFiles = len(imageCameraPairs) if numFiles < 2: raise Exception('Failed to find any image camera pairs!') if numFiles % 2 != 0: raise Exception("When solving for intrinsics, must have an even number of frames to use.") # Collect pc_align-ed cameras, unaligned disparities, and dense match files images = [] cameras = [] dispFiles = [] for it in range(numFiles/2): begFrame = options.startFrame + 2*it endFrame = begFrame + 1 batchFolderName = icebridge_common.batchFolderName(begFrame, endFrame, options.bundleLength) thisOutputFolder = os.path.join(processedFolder, batchFolderName) # Find all the cameras after bundle adjustment and pc_align. pattern = icebridge_common.getAlignedBundlePrefix(thisOutputFolder) + '*.tsai' alignedCameras = glob.glob(pattern) if len(alignedCameras) != options.bundleLength: raise Exception("Expected " + str(options.bundleLength) + " cameras, here's what " + " was obtained instead: " + " ".join(alignedCameras)) img0 = ""; cam0 = ""; img1 = ""; cam1 = "" for cam in alignedCameras: frame = icebridge_common.getFrameNumberFromFilename(cam) if begFrame == frame: img0 = imageCameraPairs[2*it][0] cam0 = cam if endFrame == frame: img1 = imageCameraPairs[2*it+1][0] cam1 = cam images.append(img0); images.append(img1) cameras.append(cam0); cameras.append(cam1) # Unaligned disparity stereoFolder = os.path.join(thisOutputFolder, 'stereo_pair_'+str(0)) currDispFiles = glob.glob(os.path.join(stereoFolder, '*unaligned-D.tif')) if len(currDispFiles) != 1: raise Exception("Expecting a single unaligned disparity file in " + stereoFolder) dispFiles.append(currDispFiles[0]) # Match files matchFiles = [] for it in range(numFiles-1): begFrame = options.startFrame + it endFrame = begFrame + 1 batchFolderName = icebridge_common.batchFolderName(begFrame, endFrame, options.bundleLength) thisOutputFolder = os.path.join(processedFolder, batchFolderName) stereoFolder = os.path.join(thisOutputFolder, 'stereo_pair_'+str(0)) DISP_PREFIX = "disp-" currMatchFiles = glob.glob(os.path.join(stereoFolder, '*' + DISP_PREFIX + '*.match')) if len(currMatchFiles) != 1: raise Exception("Expecting a single dense match file in " + stereoFolder) matchFiles.append(currMatchFiles[0]) # Create output directory for bundle adjustment and copy there the match files baDir = os.path.join(processedFolder, "bundle_intrinsics") baPrefix = os.path.join(baDir, "out") os.system("mkdir -p " + baDir) for matchFile in matchFiles: dstFile = os.path.basename(matchFile) dstFile = dstFile.replace(DISP_PREFIX, '') dstFile = os.path.join(baDir, dstFile) cmd = "cp -f " + matchFile + " " + dstFile logger.info(cmd) os.system(cmd) # The bundle adjustment cmd = "bundle_adjust " + " ".join(images) + " " + " ".join(cameras) + \ ' --reference-terrain ' + lidarFile + \ ' --disparity-list "' + " ".join(dispFiles) + '"' + \ ' --datum wgs84 -t nadirpinhole --create-pinhole-cameras --robust-threshold 2' + \ ' --camera-weight 1 --solve-intrinsics --csv-format ' + lidarCsvFormatString + \ ' --overlap-limit 1 --max-disp-error 10 --max-iterations 100 ' + \ ' --parameter-tolerance 1e-12 -o ' + baPrefix logger.info(cmd) os.system(cmd) # Generate DEMs of residuals before and after optimization projString = icebridge_common.getEpsgCode(isSouth, asString=True) for val in ['initial', 'final']: cmd = 'point2dem --t_srs ' + projString + ' --tr 2' + \ ' --csv-format 1:lon,2:lat,4:height_above_datum' + \ ' ' + baPrefix + '-' + val + '_residuals_no_loss_function_pointmap_point_log.csv' logger.info(cmd) os.system(cmd) cmd = 'point2dem --t_srs ' + projString + ' --tr 2' + \ ' --csv-format 1:lon,2:lat,4:height_above_datum' + \ ' ' + baPrefix + '-' + val +'_residuals_no_loss_function_reference_terrain.txt' logger.info(cmd) os.system(cmd) # Look at the latest written tsai file, that will be the optimized distortion file. # Force the initial rotation and translation to be the identity, this is # expected by ortho2pinhole. outFiles = filter(os.path.isfile, glob.glob(baPrefix + '*.tsai')) outFiles.sort(key=lambda x: os.path.getmtime(x)) optFile = outFiles[-1] logger.info("Reading optimized file: " + optFile) with open(optFile, 'r') as f: lines = f.readlines() for it in range(len(lines)): lines[it] = lines[it].strip() if re.match("^C\s*=\s*", lines[it]): lines[it] = "C = 0 0 0" if re.match("^R\s*=\s*", lines[it]): lines[it] = "R = 1 0 0 0 1 0 0 0 1" # Write the final desired optimized RPC file logger.info("Writing final optimized file: " + options.outputCalCamera) # Below is a bugfix, must take full path to find the dir, otherwise it may fail. os.system("mkdir -p " + os.path.dirname(os.path.abspath(options.outputCalCamera))) with open(options.outputCalCamera, 'w') as f: for line in lines: f.write(line + "\n")
# Check the inputs for f in [imageFolder, cameraFolder, lidarFolder]: if not os.path.exists(f): logger.error('Input folder ' + f + ' does not exist!') return 0 asp_system_utils.mkdir_p(outputFolder) suppressOutput = False redo = False logger.info('\nStarting processing...') # Get a list of all the input files imageCameraPairs = icebridge_common.getImageCameraPairs( imageFolder, cameraFolder, options.startFrame, options.stopFrame, logger) numFiles = len(imageCameraPairs) if numFiles < 2: raise Exception('Failed to find any image camera pairs!') # Check that the files are properly aligned lastFrame = -1 availableFrames = [] for (image, camera) in imageCameraPairs: frameNumber = icebridge_common.getFrameNumberFromFilename(image) availableFrames.append(frameNumber) if (icebridge_common.getFrameNumberFromFilename(camera) != frameNumber): logger.error('Error: input files do not align!\n' + str((image, camera)))