Ejemplo n.º 1
0
def packAndSendCompletedRun(run, logger):
    '''Assembles and compresses the deliverable parts of the run'''

    logger.info('Getting ready to pack up run ' + str(run))

    cwd = os.getcwd()
    os.chdir(run.parentFolder)

    runFolder = str(run)

    # Use symlinks to assemble a fake file structure to tar up
    assemblyFolder = run.getAssemblyFolder()
    batchFolders = run.getBatchFolderList()
    os.system('mkdir -p ' + assemblyFolder)

    # Wipe any dead symlinks, as maybe this is not the first time the assembly is made
    pattern = os.path.join(assemblyFolder, '*')
    currFiles = glob.glob(pattern)
    for filename in currFiles:
        if not os.path.exists(os.path.realpath(filename)):
            logger.info("Will wipe dead link: " + filename)
            os.system("rm -f " + filename)

    # For each batch folder, start adding links to files that we want in the tarball
    for batch in batchFolders:
        # Skip folders where we did not produce final output
        finalDemFile = os.path.join(batch, icebridge_common.blendFileName())
        if not os.path.exists(finalDemFile):
            continue

        # Need to change the name of these files when they go in the output folder
        (startFrame,
         stopFrame) = icebridge_common.getFrameRangeFromBatchFolder(batch)
        # We respect the convention below in push_to_nsidc.py.
        prefix = ('F_%05d_%05d' % (startFrame, stopFrame))
        prefix = os.path.join(assemblyFolder, prefix)
        target = prefix + '_DEM.tif'
        try:
            if os.path.exists(target):
                os.system("rm -f " + target)  # to wipe whatever was there
            os.symlink(finalDemFile, target)
        except Exception, e:
            logger.info(
                str(e) + " when doing: ln -s " + finalDemFile + " " + target)
Ejemplo n.º 2
0
def runOrtho(frame, processFolder, imageFile, bundleLength, threadText, redo,
             suppressOutput):

    os.system("ulimit -c 0")  # disable core dumps
    os.system("rm -f core.*")  # these keep on popping up
    os.system("umask 022")  # enforce files be readable by others

    # This will run as multiple processes. Hence have to catch all exceptions:
    projBounds = ()
    try:

        alignCamFile, batchFolder = \
                      icebridge_common.frameToFile(frame,
                                                   icebridge_common.alignedBundleStr() +
                                                   '*' + str(frame) + '.tsai',
                                                   processFolder, bundleLength)

        if alignCamFile == "":
            print("Could not find aligned camera for frame: " + str(frame))
            return

        # To ensure we mapproject the image fully, mosaic the several DEMs
        # around it. Keep the closest 5. Try to grab more first to account
        # for skipped frames.
        frameOffsets = [0, 1, -1, 2, -2, -3, 3, -4, 4]
        dems = []
        for offset in frameOffsets:
            demFile, batchFolder = icebridge_common.frameToFile(
                frame + offset, icebridge_common.blendFileName(),
                processFolder, bundleLength)

            # If the central DEM is missing, we are out of luck
            if offset == 0 and demFile == "":
                print("Could not find DEM for frame: " + str(frame + offset))
                return

            if offset == 0:
                demGeoInfo = asp_geo_utils.getImageGeoInfo(demFile,
                                                           getStats=False)
                projBounds = demGeoInfo[
                    'projection_bounds']  # minX maxX minY maxY

            if demFile == "":
                # Missing DEM
                continue

            if len(dems) >= 5:
                break  # too many already

            dems.append(demFile)

        demList = " ".join(dems)

        # Call this one more time, to get the current batch folder
        currDemFile, batchFolder = icebridge_common.frameToFile(
            frame, icebridge_common.blendFileName(), processFolder,
            bundleLength)

        # The names for the final results
        finalOrtho = os.path.join(batchFolder,
                                  icebridge_common.orthoFileName())
        finalOrthoPreview = os.path.join(
            batchFolder, icebridge_common.orthoPreviewFileName())

        if (not redo) and os.path.exists(finalOrtho):
            print("File exists: " + finalOrtho + ".")
        else:

            filesToWipe = []

            # If the center dem spans say 1 km, there's no way the
            # ortho can span more than 5 km, unless something is
            # seriously out of whack, such as alignment failing for
            # some neighbours. In the best case, if the center dem is
            # 1 km by 1 km, the obtained ortho will likely be 1.4 km
            # by 1 km, as an image extends beyond its stereo dem with
            # a neighbor.
            factor = float(2.0)
            projWinStr = ""
            if len(projBounds) >= 4:
                # projBounds is in the format minX maxX minY maxY
                widX = float(projBounds[1]) - float(projBounds[0])
                widY = float(projBounds[3]) - float(projBounds[2])
                projBounds = (
                    float(projBounds[0]) - factor * widX,  # minX
                    float(projBounds[1]) + factor * widX,  # maxX
                    float(projBounds[2]) - factor * widY,  # minY
                    float(projBounds[3]) + factor * widY  # maxY
                )
                projWinStr = ("--t_projwin %f %f %f %f" % \
                              (projBounds[0], projBounds[2], projBounds[1], projBounds[3]))

            # See if we have a pre-existing DEM to use as footprint
            mosaicPrefix = os.path.join(batchFolder, 'out-temp-mosaic')
            mosaicOutput = mosaicPrefix + '-tile-0.tif'
            cmd = ('dem_mosaic --hole-fill-length 500 %s %s %s -o %s' %
                   (demList, threadText, projWinStr, mosaicPrefix))
            filesToWipe.append(mosaicOutput)  # no longer needed

            print(cmd)
            localRedo = True  # The file below should not exist unless there was a crash
            asp_system_utils.executeCommand(cmd, mosaicOutput, suppressOutput,
                                            localRedo)

            # Borow some pixels from the footprint DEM,just to grow a bit the real estate
            finalFootprintDEM = os.path.join(
                batchFolder, icebridge_common.footprintFileName())
            if os.path.exists(finalFootprintDEM):
                mosaicPrefix2 = os.path.join(batchFolder, 'out-temp-mosaic2')
                mosaicOutput2 = mosaicPrefix2 + '-tile-0.tif'
                cmd = (
                    'dem_mosaic --priority-blending-length 50 %s %s %s %s -o %s'
                    % (mosaicOutput, finalFootprintDEM, threadText, projWinStr,
                       mosaicPrefix2))

                print(cmd)
                localRedo = True  # The file below should not exist unless there was a crash
                asp_system_utils.executeCommand(cmd,
                                                mosaicOutput2,
                                                suppressOutput,
                                                localRedo,
                                                noThrow=True)
                if os.path.exists(mosaicOutput2):
                    cmd = "mv -f " + mosaicOutput2 + " " + mosaicOutput
                    print(cmd)
                    os.system(cmd)

            # TODO: Look at more aggressive hole-filling. But need a testcase.

            filesToWipe += glob.glob(mosaicPrefix + '*' + '-log-' + '*')

            # First mapproject to create a tif image with 4 channels.
            # Then pull 3 channels and compress them as jpeg, while keeping the
            # image a geotiff.

            tempOrtho = os.path.join(
                batchFolder,
                icebridge_common.orthoFileName() + "_tmp.tif")

            # There is no need for this file to exist unless it is stray junk
            if os.path.exists(tempOrtho):
                os.remove(tempOrtho)

            # Run mapproject. The grid size is auto-determined.
            cmd = (
                'mapproject --no-geoheader-info %s %s %s %s %s' %
                (mosaicOutput, imageFile, alignCamFile, tempOrtho, threadText))
            print(cmd)
            asp_system_utils.executeCommand(cmd, tempOrtho, suppressOutput,
                                            redo)
            filesToWipe.append(tempOrtho)

            # This makes the images smaller than Rose's by a factor of about 4,
            # even though both types are jpeg compressed. Rose's images filtered
            # through this command also get compressed by a factor of 4.
            # I conclude that the jpeg compression used by Rose was not as
            # aggressive as the one used in gdal_translate, but there is no
            # apparent knob to control that.
            cmd = "gdal_translate -b 1 -b 2 -b 3 -co compress=jpeg " + tempOrtho + " " + finalOrtho
            print(cmd)
            asp_system_utils.executeCommand(cmd, finalOrtho, suppressOutput,
                                            redo)

            # Clean up extra files
            for fileName in filesToWipe:
                if os.path.exists(fileName):
                    print("Removing: " + fileName)
                    os.remove(fileName)

        if (not redo) and os.path.exists(finalOrthoPreview):
            print("File exists: " + finalOrthoPreview + ".")
        else:
            cmd = 'gdal_translate -scale -outsize 25% 25% -of jpeg ' + finalOrtho + \
                  ' ' + finalOrthoPreview
            print(cmd)
            asp_system_utils.executeCommand(cmd, finalOrthoPreview,
                                            suppressOutput, redo)

    except Exception as e:
        print('Ortho creation failed!\n' + str(e) + ". " +
              str(traceback.print_exc()))

    os.system("rm -f core.*")  # these keep on popping up

    # To ensure we print promptly what we did so far
    sys.stdout.flush()
Ejemplo n.º 3
0
def packAndSendCompletedRun(run, logger):
    '''Assembles and compresses the deliverable parts of the run'''
    
    logger.info('Getting ready to pack up run ' + str(run))
    
    cwd = os.getcwd()
    os.chdir(run.parentFolder)
    
    runFolder = str(run)

    # Use symlinks to assemble a fake file structure to tar up
    assemblyFolder = run.getAssemblyFolder()
    batchFolders   = run.getBatchFolderList()
    os.system('mkdir -p ' + assemblyFolder)

    # Wipe any dead symlinks, as maybe this is not the first time the assembly is made
    pattern = os.path.join(assemblyFolder, '*')
    currFiles = glob.glob(pattern)
    for filename in currFiles:
        if not os.path.exists(os.path.realpath(filename)):
            logger.info("Will wipe dead link: " + filename)
            os.system("rm -f " + filename)
            
    # For each batch folder, start adding links to files that we want in the tarball
    for batch in batchFolders:
        # Skip folders where we did not produce final output
        finalDemFile = os.path.join(batch, icebridge_common.blendFileName())
        if not os.path.exists(finalDemFile):
            continue
        
        # Need to change the name of these files when they go in the output folder
        (startFrame, stopFrame) = icebridge_common.getFrameRangeFromBatchFolder(batch)
        # We respect the convention below in push_to_nsidc.py.
        prefix = ('F_%05d_%05d' % (startFrame, stopFrame))
        prefix = os.path.join(assemblyFolder, prefix)
        target = prefix + '_DEM.tif'
        try:
            if os.path.exists(target):
                os.system("rm -f " + target) # to wipe whatever was there
            os.symlink(finalDemFile, target)
        except Exception as e:
            logger.info(str(e) + " when doing: ln -s " + finalDemFile + " " + target)
    
    # Tar up the assembled files and send them at the same time using the shiftc command
    # - No need to use a compression algorithm here
    fileName = run.getOutputTarName()
    lfePath  = os.path.join(REMOTE_OUTPUT_FOLDER, fileName)

    logger.info('Sending run to lfe...')

    cmd = "ssh lfe 'rm -f " + stripHost(lfePath) + "' 2>/dev/null"

    logger.info(cmd)
    os.system(cmd)

    cmd = 'shiftc --wait -d -r --dereference --create-tar ' + \
          os.path.join(runFolder, os.path.basename(assemblyFolder)) + ' ' + lfePath

    # Command to transfer the files as they are in the batch dirs without symlink
    #cmd = 'shiftc --wait -d -r --include=\'^.*?' + icebridge_common.blendFileName() + \
    #      '$\' --create-tar ' + runFolder + \
    #      ' ' + lfePath

    try:
        robust_shiftc(cmd, logger)
    except Exception as e:
        logger.info(str(e))
        raise Exception('Failed to pack/send results for run ' + str(run) + \
                    '. Maybe not all sym links are valid.')
    
    os.chdir(cwd)
    
    logger.info('Finished sending run to lfe.')
    # Collect per-batch information
    batchInfoPath = os.path.join(options.outputFolder, 'batchInfoSummary.csv')
    failedBatchPath = os.path.join(options.outputFolder, 'failedBatchList.csv')
    print("Writing statistics to: " + batchInfoPath)
    print("Writing failures to: " + failedBatchPath)
    batchInfoLog = open(batchInfoPath, 'w')
    failureLog = open(failedBatchPath, 'w')

    # Write the header for the batch log file
    batchInfoLog.write('# startFrame, stopFrame, centerLon, centerLat, meanAlt, ' +
                       'meanLidarDiff, meanInterDiff, meanFireDiff, meanFireLidarDiff, ' +
                       'percentValid, meanBlendDiff, meanBlendDiffInFireballFootprint, ' + \
                       'corrSearchWid, corrMem(GB), corrElapsedTime(minutes)\n')
    failureLog.write('# startFrame, stopFrame, errorCode, errorText\n')

    demList = run.getOutputFileList(icebridge_common.blendFileName())
    for (dem, frames) in demList:

        demFolder = os.path.dirname(dem)

        # Handle frame range option
        if (frames[0] < options.startFrame):
            continue
        if (frames[1] > options.stopFrame):
            break

        # Progress indication
        if frames[0] % 100 == 0:
            print("Frame: " + str(frames[0]))
            batchInfoLog.flush()  # for instant gratification
            failureLog.flush()
def runOrtho(frame, processFolder, imageFile, bundleLength, cameraMounting,
             threadText, redo, suppressOutput):

    os.system("ulimit -c 0") # disable core dumps
    os.system("rm -f core.*") # these keep on popping up
    os.system("umask 022")   # enforce files be readable by others

    # This will run as multiple processes. Hence have to catch all exceptions:
    projBounds = ()
    try:

        # Retrieve the aligned camera file
        alignCamFile, batchFolder = \
                      icebridge_common.frameToFile(frame,
                                                   icebridge_common.alignedBundleStr() + 
                                                   '*' + str(frame) + '.tsai',
                                                   processFolder, bundleLength)

        if alignCamFile == "":
            print("Could not find aligned camera for frame: " + str(frame))
            return

        # To ensure we mapproject the image fully, mosaic the several DEMs
        # around it. Keep the closest 5. Try to grab more first to account
        # for skipped frames.
        frameOffsets = [0, 1, -1, 2, -2, -3, 3, -4, 4]
        dems = []
        for offset in frameOffsets:
            # Find the DEM file for the desired frame
            demFile, batchFolder = icebridge_common.frameToFile(frame + offset,
                                                                icebridge_common.blendFileName(),
                                                                processFolder, bundleLength)
            # If the central DEM is missing, we are out of luck
            if offset == 0 and demFile == "":
                print("Could not find blended DEM for frame: " + str(frame + offset))
                return

            if offset == 0:
                demGeoInfo = asp_geo_utils.getImageGeoInfo(demFile, getStats=False)
                projBounds = demGeoInfo['projection_bounds'] # minX maxX minY maxY
                
            if demFile == "":
                # Missing DEM
                continue

            if len(dems) >= 5:
                break # too many already
            
            dems.append(demFile)
            
        demList = " ".join(dems)

        # Call this one more time, to get the current batch folder
        currDemFile, batchFolder = icebridge_common.frameToFile(frame,
                                                                icebridge_common.blendFileName(),
                                                                processFolder, bundleLength)

        # The names for the final results
        finalOrtho        = os.path.join(batchFolder, icebridge_common.orthoFileName())
        finalOrthoPreview = os.path.join(batchFolder, icebridge_common.orthoPreviewFileName())
        
        if (not redo) and os.path.exists(finalOrtho):
            print("File exists: " + finalOrtho + ".")
        else:

            filesToWipe = []

            # If the center dem spans say 1 km, there's no way the
            # ortho can span more than 5 km, unless something is
            # seriously out of whack, such as alignment failing for
            # some neighbours. In the best case, if the center dem is
            # 1 km by 1 km, the obtained ortho will likely be 1.4 km
            # by 1 km, as an image extends beyond its stereo dem with
            # a neighbor.
            factor = float(2.0)
            projWinStr = ""
            if len(projBounds) >= 4:
                # projBounds is in the format minX maxX minY maxY
                widX = float(projBounds[1]) - float(projBounds[0])
                widY = float(projBounds[3]) - float(projBounds[2])
                projBounds = ( 
                    float(projBounds[0]) - factor*widX, # minX
                    float(projBounds[1]) + factor*widX, # maxX
                    float(projBounds[2]) - factor*widY, # minY
                    float(projBounds[3]) + factor*widY  # maxY
                    ) 
                projWinStr = ("--t_projwin %f %f %f %f" % \
                              (projBounds[0], projBounds[2], projBounds[1], projBounds[3]))
                
            # See if we have a pre-existing DEM to use as footprint
            mosaicPrefix = os.path.join(batchFolder, 'out-temp-mosaic')
            mosaicOutput = mosaicPrefix + '-tile-0.tif'
            cmd = ('dem_mosaic --hole-fill-length 500 %s %s %s -o %s' 
                   % (demList, threadText, projWinStr, mosaicPrefix))
            filesToWipe.append(mosaicOutput) # no longer needed

            # Generate the DEM mosaic
            print(cmd)
            localRedo = True # The file below should not exist unless there was a crash
            asp_system_utils.executeCommand(cmd, mosaicOutput, suppressOutput, localRedo)

            # Borow some pixels from the footprint DEM,just to grow a bit the real estate
            finalFootprintDEM = os.path.join(batchFolder,
                                             icebridge_common.footprintFileName())
            if os.path.exists(finalFootprintDEM):
                mosaicPrefix2 = os.path.join(batchFolder, 'out-temp-mosaic2')
                mosaicOutput2 = mosaicPrefix2 + '-tile-0.tif'
                cmd = ('dem_mosaic --priority-blending-length 50 %s %s %s %s -o %s' 
                       % (mosaicOutput, finalFootprintDEM, threadText, projWinStr, mosaicPrefix2))
                
                print(cmd)
                localRedo = True # The file below should not exist unless there was a crash
                asp_system_utils.executeCommand(cmd, mosaicOutput2, suppressOutput, localRedo,
                                                noThrow = True)
                if os.path.exists(mosaicOutput2):
                    cmd = "mv -f " + mosaicOutput2 + " " + mosaicOutput
                    print(cmd) 
                    os.system(cmd)
                
            # TODO: Look at more aggressive hole-filling. But need a testcase.
            
            filesToWipe += glob.glob(mosaicPrefix + '*' + '-log-' + '*')

            # First mapproject to create a tif image with 4 channels.
            # Then pull 3 channels and compress them as jpeg, while keeping the
            # image a geotiff.

            tempOrtho = os.path.join(batchFolder, icebridge_common.orthoFileName() + "_tmp.tif")

            # There is no need for this file to exist unless it is stray junk
            if os.path.exists(tempOrtho):
                os.remove(tempOrtho)

            # If needed, generate a temporary camera file to correct a mounting rotation.
            # - When the camera mount is rotated 90 degrees stereo is run on a corrected version
            #   but ortho needs to work on the original uncorrected jpeg image.
            tempCamFile = alignCamFile + '_temp_rot.tsai'
            tempCamFile = createRotatedCameraFile(alignCamFile, tempCamFile, cameraMounting)
            # Run mapproject. The grid size is auto-determined.
            cmd = ('mapproject --no-geoheader-info %s %s %s %s %s' 
                   % (mosaicOutput, imageFile, tempCamFile, tempOrtho, threadText))
            print(cmd)
            asp_system_utils.executeCommand(cmd, tempOrtho, suppressOutput, redo)
            # Set temporary files to be cleaned up
            filesToWipe.append(tempOrtho)
            if tempCamFile != alignCamFile:
                filesToWipe.append(tempCamFile)

            # This makes the images smaller than Rose's by a factor of about 4,
            # even though both types are jpeg compressed. Rose's images filtered
            # through this command also get compressed by a factor of 4.
            # I conclude that the jpeg compression used by Rose was not as
            # aggressive as the one used in gdal_translate, but there is no
            # apparent knob to control that. 
            cmd = "gdal_translate -b 1 -b 2 -b 3 -co compress=jpeg " + tempOrtho + " " + finalOrtho
            print(cmd)
            asp_system_utils.executeCommand(cmd, finalOrtho, suppressOutput, redo)
            
            # Clean up extra files
            for fileName in filesToWipe:
                if os.path.exists(fileName):
                    print("Removing: " + fileName)
                    os.remove(fileName)

        if (not redo) and os.path.exists(finalOrthoPreview):
            print("File exists: " + finalOrthoPreview + ".")
        else:
            cmd = 'gdal_translate -scale -outsize 25% 25% -of jpeg ' + finalOrtho + \
                  ' ' + finalOrthoPreview 
            print(cmd)
            asp_system_utils.executeCommand(cmd, finalOrthoPreview, suppressOutput, redo)
            
    except Exception as e:
        print('Ortho creation failed!\n' + str(e) + ". " + str(traceback.print_exc()))

    os.system("rm -f core.*") # these keep on popping up

    # To ensure we print promptly what we did so far
    sys.stdout.flush()
def generateFlightSummary(run, options):
    '''Generate a folder containing handy debugging files including output thumbnails'''

    # Copy logs to the output folder
    print 'Copying log files...'
    badImageFolder = os.path.join(options.outputFolder, 'badImages')
    runFolder = run.getFolder()
    procFolder = run.getProcessFolder()
    navCameraFolder = run.getNavCameraFolder()
    os.system('mkdir -p ' + options.outputFolder)
    os.system('mkdir -p ' + badImageFolder)

    packedErrorLog = os.path.join(runFolder, 'packedErrors.log')
    if os.path.exists(packedErrorLog):
        try:
            shutil.copy(packedErrorLog, options.outputFolder)
        except Exception as e:
            # In case it complains about copying a file onto itself
            print("Warning: " + str(e))

    if not options.skipKml:
        # Copy the input camera kml file
        camerasInKmlPath = os.path.join(procFolder, 'cameras_in.kml')
        try:
            shutil.copy(camerasInKmlPath, options.outputFolder)
        except Exception as e:
            # In case it complains about copying a file onto itself
            print("Warning: " + str(e))

        # Copy the input camera kml file
        navCamerasKmlPath = os.path.join(navCameraFolder, 'nav_cameras.kml')
        try:
            shutil.copy(navCamerasKmlPath, options.outputFolder)
        except Exception as e:
            # In case it complains about copying a file onto itself
            print("Warning: " + str(e))

        # Create a merged version of all the bundle adjusted camera files
        # - The tool currently includes cameras more than once if they appear
        #   in multiple bundles.
        print 'Merging output camera kml files...'
        cmd = "find " + procFolder + " -name cameras_out.kml"
        p = subprocess.Popen(cmd.split(),
                             stdout=subprocess.PIPE,
                             shell=False,
                             universal_newlines=True)
        textOutput, err = p.communicate()
        camKmlFiles = textOutput.replace('\n', ' ')

        # Write the list of files to process to disk. Otherwise, if we just
        # pass the full list on the command line, it may be too big
        # and the call will fail.
        kmlList = os.path.join(options.outputFolder, 'kml_list.txt')
        print("Writing: " + kmlList)
        with open(kmlList, 'w') as f:
            for filename in sorted(camKmlFiles.split()):
                filename = filename.strip()
                if filename == "":
                    continue
                f.write(filename + "\n")

        outputKml = os.path.join(options.outputFolder, 'cameras_out.kml')
        scriptPath = icebridge_common.fullPath('merge_orbitviz.py')
        cmd = 'python ' + scriptPath + ' ' + outputKml + ' -list ' + kmlList
        print(cmd)
        os.system(cmd)

        # Generate lidar kml files
        print("Generating lidar kml files")
        LIDAR_POINT_SKIP = 1527
        lidarFiles = run.getLidarList(prependFolder=True)
        lidarOutputFolder = os.path.join(options.outputFolder, 'lidar')
        os.system('mkdir -p ' + lidarOutputFolder)
        for f in lidarFiles:
            inputPath = os.path.splitext(f)[0] + '.csv'
            outputPath = os.path.join(lidarOutputFolder,
                                      os.path.basename(f) + '.kml')
            args = [
                inputPath, outputPath, '--skip',
                str(LIDAR_POINT_SKIP), '--color', 'red'
            ]
            if not os.path.exists(outputPath):  # Don't recreate these files
                print("Generating: " + outputPath
                      )  # This can be very slow, so print what is going on
                try:
                    lvis2kml.main(args)
                except Exception as e:
                    # Do not let this make our life miserable
                    print("Problem: " + str(e))

    # Collect per-batch information
    batchInfoPath = os.path.join(options.outputFolder, 'batchInfoSummary.csv')
    failedBatchPath = os.path.join(options.outputFolder, 'failedBatchList.csv')
    print("Writing statistics to: " + batchInfoPath)
    print("Writing failures to: " + failedBatchPath)
    batchInfoLog = open(batchInfoPath, 'w')
    failureLog = open(failedBatchPath, 'w')

    # Write the header for the batch log file
    batchInfoLog.write('# startFrame, stopFrame, centerLon, centerLat, meanAlt, ' +
                       'meanLidarDiff, meanInterDiff, meanFireDiff, meanFireLidarDiff, ' +
                       'percentValid, meanBlendDiff, meanBlendDiffInFireballFootprint, ' + \
                       'corrSearchWid, corrMem(GB), corrElapsedTime(minutes)\n')
    failureLog.write('# startFrame, stopFrame, errorCode, errorText\n')

    demList = run.getOutputFileList(icebridge_common.blendFileName())
    for (dem, frames) in demList:

        demFolder = os.path.dirname(dem)

        # Handle frame range option
        if (frames[0] < options.startFrame):
            continue
        if (frames[1] > options.stopFrame):
            break

        # Progress indication
        if frames[0] % 100 == 0:
            print("Frame: " + str(frames[0]))
            batchInfoLog.flush()  # for instant gratification
            failureLog.flush()

        # Read in blend results which are not part of the consolidated stats file
        blendDiffPath = os.path.join(demFolder, 'out-blend-DEM-diff.csv')
        try:
            blendDiffResults = icebridge_common.readGeodiffOutput(
                blendDiffPath)
        except:
            blendDiffResults = {'Mean': -999}

        # Read in blend results which are not part of the consolidated stats file
        # for the blend done in the fireball footprint
        fireballBlendDiffPath = os.path.join(
            demFolder, 'out-blend-fb-footprint-diff.csv')

        try:
            fireballBlendDiffResults = icebridge_common.readGeodiffOutput(
                fireballBlendDiffPath)
        except:
            fireballBlendDiffResults = {'Mean': -999}

        runStatsFile = os.path.join(demFolder,
                                    icebridge_common.getRunStatsFile())
        statsLine = icebridge_common.readStats(runStatsFile)

        # All of the other results should be in a consolidated stats file
        consolidatedStatsPath = os.path.join(demFolder,
                                             'out-consolidated_stats.txt')

        if not os.path.exists(consolidatedStatsPath):
            # Stats file not present, recreate it.

            print 'Recreating missing stats file: ' + consolidatedStatsPath

            # Get paths to the files of interest
            # This logic must be sync-ed up with cleanBatch().
            lidarDiffPath = os.path.join(demFolder, 'out-diff.csv')
            interDiffPath = os.path.join(demFolder,
                                         'out_inter_diff_summary.csv')
            fireDiffPath = os.path.join(demFolder,
                                        'out_fireball_diff_summary.csv')
            fireLidarDiffPath = os.path.join(demFolder,
                                             'out_fireLidar_diff_summary.csv')
            fractionValidPath = os.path.join(demFolder,
                                             'valid_pixel_fraction.txt')
            process_icebridge_batch.consolidateStats(
                lidarDiffPath, interDiffPath, fireDiffPath, fireLidarDiffPath,
                dem, consolidatedStatsPath, fractionValidPath, None,
                options.skipGeo)
        # Now the consolidated file should always be present

        with open(consolidatedStatsPath, 'r') as f:
            statsText = f.read()

        # Write info to summary file
        try:
            batchInfoLog.write(
                '%d, %d, %s, %f, %f, %s\n' %
                (frames[0], frames[1], statsText, blendDiffResults['Mean'],
                 fireballBlendDiffResults['Mean'], statsLine))
        except:
            # Bugfix for corrupted data
            print("Problem parsing frame: " + str(frames[0]))

        # Keep a list of batches that did not generate an output DEM
        parts = statsText.split(',')
        if len(parts) <= 2:
            print("Cannot parse " + consolidatedStatsPath + ", skipping.")
        else:
            if (float(parts[0]) == 0) and (float(parts[1]) == 0) and (float(
                    parts[2]) == -999):

                if os.path.exists(dem):
                    # Handle the case where the statistics are bad for some reason
                    errorCode = 0
                    errorText = 'Success but statistics are bad'
                else:  # A real failure, figure out the cause
                    batchFolder = os.path.dirname(dem)
                    (errorCode, errorText) = getFailureCause(batchFolder)
                    #print str((errorCode, errorText))
                #if errorCode < 0: # Debug code for unknown errors
                #print str((errorCode, errorText))
                #print statsText
                #print batchFolder
                #raise Exception('DEBUG')
                failureLog.write('%d, %d, %d, %s\n' %
                                 (frames[0], frames[1], errorCode, errorText))

        # Make a link to the DEM thumbnail file in our summary folder
        hillshadePath = os.path.join(demFolder,
                                     'out-blend-DEM_HILLSHADE_browse.tif')
        if os.path.exists(hillshadePath):
            thumbName = ('dem_%05d_%05d_browse.tif' % (frames[0], frames[1]))
            thumbPath = os.path.join(options.outputFolder, thumbName)
            icebridge_common.makeSymLink(hillshadePath,
                                         thumbPath,
                                         verbose=False)
        else:
            # If the DEM thumbnail does not exist, look for the input frame thumbnail.
            inPath = os.path.join(demFolder, 'first_image_browse.tif')
            thumbName = ('input_%05d_browse.tif' % (frames[0]))
            thumbPath = os.path.join(badImageFolder, thumbName)
            if os.path.exists(inPath):
                icebridge_common.makeSymLink(inPath, thumbPath, verbose=False)

        # Make a link to the ortho thumbnail file in our summary folder
        orthoPath = os.path.join(demFolder,
                                 icebridge_common.orthoPreviewFileName())
        if os.path.exists(orthoPath):
            thumbName = ('ortho_%05d_%05d_browse.jpg' % (frames[0], frames[1]))
            thumbPath = os.path.join(options.outputFolder, thumbName)
            icebridge_common.makeSymLink(orthoPath, thumbPath, verbose=False)

    # End loop through expected DEMs and writing log files
    batchInfoLog.close()
    failureLog.close()

    print 'Finished generating flight summary in folder: ' + options.outputFolder
    print("Wrote: " + batchInfoPath)
    print("Wrote: " + failedBatchPath)