Example #1
0
def loadFigImage(im):
    """
    Return a matplotlib `Figure` with the given raster image spanning the plot extents
    and with data coordinates equal to pixel coordinates. All axis and labels are
    hidden.
    
    :param im: either path to image file, or RGB image array in uint8 or uint16 type
    :rtype: tuple(Figure,Axes)
    """
    if isinstance(im, string_types):
        im = loadImage(im)
    im = image2mpl(im)
    h, w = im.shape[0], im.shape[1]
    dpi = 80
    fig = plt.figure(figsize=(w / dpi, h / dpi), dpi=dpi)
    ax = plt.Axes(fig, [0, 0, 1, 1])
    ax.set_xlim(0, w)
    ax.set_ylim(0, h)
    ax.invert_yaxis()
    ax.set_axis_off()
    fig.add_axes(ax)
    if im.ndim == 2:
        fig.figimage(im, cmap=cm.gray)
    else:
        fig.figimage(im)
    fig._dontSetColors = True

    return fig, ax
Example #2
0
def maskAllInFolder(folderPath, outputFolderPath, ext='jpg', maskFn=maskStarfield, preserveExif=True):
    """
    Masks the starfield of all images in `folderPath` and stores the masked images
    in `outputFolderPath`.
    
    :param str ext: image extension
    :param function maskFn: the masking function to use, 
        by default the standard algorithm from :mod:`auromat.solving.masking`
    :param bool preserveExif: whether to copy EXIF data (exiftool must be installed)
    """
    if preserveExif:
        et = exiftool.ExifTool()
        et.start()
    
    for imageFilename in os.listdir(folderPath):
        imagePath = os.path.join(folderPath, imageFilename)
        maskedPath = os.path.join(outputFolderPath, os.path.splitext(imageFilename)[0] + '.' + ext)
        mask, _ = maskFn(imagePath)
        im = loadImage(imagePath)
        im[~mask] = 0
        saveImage(maskedPath, im)
        
        if preserveExif:
            et.copy_tags(imagePath, maskedPath)
    
    if preserveExif:
        et.terminate()
Example #3
0
def maskAllInFolder(folderPath,
                    outputFolderPath,
                    ext='jpg',
                    maskFn=maskStarfield,
                    preserveExif=True):
    """
    Masks the starfield of all images in `folderPath` and stores the masked images
    in `outputFolderPath`.
    
    :param str ext: image extension
    :param function maskFn: the masking function to use, 
        by default the standard algorithm from :mod:`auromat.solving.masking`
    :param bool preserveExif: whether to copy EXIF data (exiftool must be installed)
    """
    if preserveExif:
        et = exiftool.ExifTool()
        et.start()

    for imageFilename in os.listdir(folderPath):
        imagePath = os.path.join(folderPath, imageFilename)
        maskedPath = os.path.join(
            outputFolderPath,
            os.path.splitext(imageFilename)[0] + '.' + ext)
        mask, _ = maskFn(imagePath)
        im = loadImage(imagePath)
        im[~mask] = 0
        saveImage(maskedPath, im)

        if preserveExif:
            et.copy_tags(imagePath, maskedPath)

    if preserveExif:
        et.terminate()
Example #4
0
 def img_unmasked(self):
     if self._img_unmasked is None:
         rgb = loadImage(self._imagePath)
         if rgb.shape[0] != rgb.shape[1]:
             # contains caption below image, cut it off
             rgb = rgb[:rgb.shape[1],:]
             assert rgb.shape == (rgb.shape[1],rgb.shape[1],3)
         self._img_unmasked = rgb
     return self._img_unmasked
Example #5
0
def estimateArcSecRange(imagePath, imageSize=None):
    focal35mm = readFocalLength35mm(imagePath)
    if focal35mm is None:
        return None
    focal35mmLow = focal35mm * 0.9
    focal35mmHigh = focal35mm * 1.1
    if imageSize is None:
        _, w, = loadImage(imagePath).shape
    else:
        _, w = imageSize
    # see http://cbellh47.blogspot.nl/2010/01/astrometry-101-pixel-scale.html
    pixelSizeMm = 35 / w
    arcsecLow = (pixelSizeMm / focal35mmHigh * u.rad).to(u.arcsec).value
    arcsecHigh = (pixelSizeMm / focal35mmLow * u.rad).to(u.arcsec).value
    return (arcsecLow, arcsecHigh)
Example #6
0
    def _copyTempFiles(fitsWcsHeader, name):
        if not keepTempFiles:
            return
        if os.path.exists(objsPath):
            p = os.path.join(debugOutputFolder,
                             objsBaseOutput + '_' + name + '.jpg')
            saveImage(p, loadImage(objsPath))  # convert png to jpg

        if not fitsWcsHeader and os.path.exists(axyPath):
            p = os.path.join(debugOutputFolder, axyBaseOutput + name + '.axy')
            shutil.copy(axyPath, p)

        if not fitsWcsHeader and os.path.exists(logPath):
            p = os.path.join(debugOutputFolder, logBaseOutput + name + '.log')
            shutil.copy(logPath, p)
Example #7
0
def correctLensDistortion(imagePath,
                          undistImagePath,
                          lensfunDbObj=None,
                          mod=None,
                          exiftoolObj=None,
                          preserveExif=True,
                          minAcceptedScore=MIN_ACCEPTED_SCORE,
                          **saveImageKws):
    """
    Correct lens distortion of an image using its EXIF headers and the lensfun library.
    If the camera or lens are not found in the lensfun database, an exception is raised.
    
    :param undistImagePath: path to output image; folders must already exist! 
    :param exiftoolObj: if not None, use the given exiftool object (must have nums=False)
    :param lensfunDbObj: if not None, use the given lensfun.Database object
    :param mod: if not None, use this Modifier instead of calling getLensfunModifier()
                Note: lensfunDbObj is not used in this case.
    :raise ValueError: when lens wasn't found in EXIF data
    :raise CameraNotFoundInDBError: when the camera wasn't found in lensfun database
    :raise LensNotFoundInDBError: when the lens wasn't found in lensfun database  
    """
    im = loadImage(imagePath)
    if mod is None:
        height, width = im.shape[0], im.shape[1]
        mod, _, _ = getLensfunModifier(imagePath,
                                       width,
                                       height,
                                       lensfunDbObj,
                                       exiftoolObj,
                                       minAcceptedScore=minAcceptedScore)

    undistCoords = mod.apply_geometry_distortion()
    imUndistorted = lensfunpy.util.remap(im, undistCoords)
    saveImage(undistImagePath, imUndistorted, **saveImageKws)

    # TODO set a flag indicating that lens correction has been done
    #      there is no standard flag for that
    #      the closest one is Xmp.digiKam.LensCorrectionSettings
    #      see http://api.kde.org/extragear-api/graphics-apidocs/digikam/html/classDigikam_1_1LensFunFilter.html
    #      adding custom XMP tags requires to change some exiftool config file...
    if preserveExif:
        if exiftoolObj:
            exiftoolObj.copy_tags(imagePath, undistImagePath)
        else:
            with exiftool.ExifTool() as et:
                et.copy_tags(imagePath, undistImagePath)
Example #8
0
def correctLensDistortion(imagePath, undistImagePath, 
                          lensfunDbObj=None, mod=None, 
                          exiftoolObj=None, preserveExif=True,
                          **saveImageKws):
    """
    Correct lens distortion of an image using its EXIF headers and the lensfun library.
    If the camera or lens are not found in the lensfun database, an exception is raised.
    
    :param undistImagePath: path to output image; folders must already exist! 
    :param exiftoolObj: if not None, use the given exiftool object (must have nums=False)
    :param lensfunDbObj: if not None, use the given lensfun.Database object
    :param mod: if not None, use this Modifier instead of calling getLensfunModifier()
                Note: lensfunDbObj is not used in this case.
    :raise ValueError: when lens wasn't found in EXIF data
    :raise CameraNotFoundInDBError: when the camera wasn't found in lensfun database
    :raise LensNotFoundInDBError: when the lens wasn't found in lensfun database  
    """
    im = loadImage(imagePath)
    if mod is None:
        height, width = im.shape[0], im.shape[1]
        mod, _, _ = getLensfunModifier(imagePath, width, height, lensfunDbObj, exiftoolObj)

    undistCoords = mod.apply_geometry_distortion()    
    imUndistorted = lensfunpy.util.remap(im, undistCoords)
    saveImage(undistImagePath, imUndistorted, **saveImageKws)
    
    # TODO set a flag indicating that lens correction has been done
    #      there is no standard flag for that
    #      the closest one is Xmp.digiKam.LensCorrectionSettings
    #      see http://api.kde.org/extragear-api/graphics-apidocs/digikam/html/classDigikam_1_1LensFunFilter.html
    #      adding custom XMP tags requires to change some exiftool config file...
    if preserveExif:
        if exiftoolObj:
            exiftoolObj.copy_tags(imagePath, undistImagePath)
        else:
            with exiftool.ExifTool() as et:
                et.copy_tags(imagePath, undistImagePath)
Example #9
0
def solveImage(imagePath,
               channel=None,
               maskingFn=maskStarfield,
               sigma=None,
               solveTimeout=60 * 5,
               debugOutputFolder=None,
               noAstrometryPlots=False,
               arcsecRange=None,
               astrometryBinPath=None,
               useModifiedPath=False,
               parameters=['xy', 'xy2', 'xy4', 's'],
               pixelError=10,
               oddsToSolve=None,
               verbose=False):
    """
    Tries different combinations to solve the image using astrometry.net.
    
    :param imagePath:
    :param channel: the channel to use for source extraction
        'R','G','B', or None for combining all channels into a grayscale image
    :param maskingFn: function to use for masking the given image,
                      if None, masking is skipped
    :param sigma: noise level of the image (optional)
    :param solveTimeout: maximum time in seconds after which astrometry.net is killed
    :param debugOutputFolder: if given, the path to which debug files are written
    :param noAstrometryPlots: whether to let astrometry.net generate plots,
                              if True, then debugOutputFolder must be given 
    :param arcsecRange: tuple(low,high), if not given, then it is guessed from
                        the image file if possible
    :param astrometryBinPath: path to the bin/ folder of astrometry.net;
                              if not given, then whatever is in PATH will be used
    :param bool useModifiedPath: invokes astrometry.net with /usr/bin/env PATH=os.environ['PATH']
                                 This may be useful when the PATH was modified after launching Python.
    :param int pixelError: size of pixel positional error, use higher values (e.g. 10)
                           if image contains star trails (ISS images)
    :param oddsToSolve: default 1e9, see astrometry.net docs
    :rtype: dictionary containing FITS WCS header, or None if solving failed
    """
    imageBase = os.path.splitext(os.path.basename(imagePath))[0]

    tmpDir = tempfile.mkdtemp()
    tmpDirAstrometry = os.path.join(tmpDir, 'astrometry')

    # TODO we need to return which solving strategy (downsampling, extractor) was used
    #      -> this should probably be added to the .axy header
    #      as astrometry.net repeatedly downsamples in certain cases it
    #      is hard to determine the correct downsampling that was used
    #      -> see also http://trac.astrometry.net/ticket/1117

    if maskingFn is None:
        imageMaskedPath = imagePath
        maskedSuffix = ''
        imageSize = None
        if channel is not None:
            raise NotImplementedError
    else:
        maskedSuffix = '_masked'
        imageMaskedPath = os.path.join(tmpDir,
                                       imageBase + maskedSuffix + ".png")

        # step 1: create starfield-masked image
        t0 = time.time()

        if not debugOutputFolder:
            debugPathPrefix = None
        else:
            debugPathPrefix = os.path.join(debugOutputFolder, imageBase) + '_'
        mask, sigma_ = maskingFn(imagePath, debugPathPrefix=debugPathPrefix)
        if sigma is None:
            sigma = sigma_
        im = loadImage(imagePath)
        im[~mask] = 0
        if channel is None:
            pass
        elif channel.lower() == 'r':
            im = im[:, :, 0]
        elif channel.lower() == 'g':
            im = im[:, :, 1]
        elif channel.lower() == 'b':
            im = im[:, :, 2]
        else:
            raise ValueError(
                'channel is "{}" but must be R,G,B or None'.format(channel))
        saveImage(imageMaskedPath, im)
        imageSize = (im.shape[0], im.shape[1])

        print('masking:', time.time() - t0, 's')

        if debugOutputFolder is not None:
            shutil.copy(imageMaskedPath, debugOutputFolder)

    # step 2: invoke astrometry.net

    # Note that astrometry.net assumes that images are taken from earth.
    # As the ISS is very near to earth, the error is most likely below the
    # accuracy that astrometry.net provides and also below the common pixel
    # resolution of images and can therefore be ignored.
    keepTempFiles = False if debugOutputFolder is None else True

    if debugOutputFolder is None:
        noAstrometryPlots = True

    logFilename = imageBase + maskedSuffix + '.log'
    logFilenameOutput = imageBase + '.log'
    logPath = os.path.join(tmpDirAstrometry, logFilename)
    logBaseOutput = imageBase + '_'

    objsBase = imageBase + maskedSuffix + '_objs'
    objsBaseOutput = imageBase + '_objs'
    objsPath = os.path.join(tmpDirAstrometry, objsBase + '.png')

    indxBase = imageBase + maskedSuffix + '_indx'
    indxBaseOutput = imageBase + '_indx'
    indxFilename = indxBase + '.png'
    indxFilenameOutput = indxBaseOutput + '.png'
    indxPath = os.path.join(tmpDirAstrometry, indxFilename)

    matchFilename = imageBase + maskedSuffix + '.match'
    matchFilenameOutput = imageBase + '.match'
    matchPath = os.path.join(tmpDirAstrometry, matchFilename)

    indxXyFilename = imageBase + maskedSuffix + '.xyls'
    indxXyFilenameOutput = imageBase + '.xyls'
    indxXyPath = os.path.join(tmpDirAstrometry, indxXyFilename)

    axyFilename = imageBase + maskedSuffix + '.axy'
    axyFilenameOutput = imageBase + '.axy'
    axyPath = os.path.join(tmpDirAstrometry, axyFilename)
    axyBaseOutput = imageBase + '_'

    corrFilename = imageBase + maskedSuffix + '.corr'
    corrFilenameOutput = imageBase + '.corr'
    corrPath = os.path.join(tmpDirAstrometry, corrFilename)

    # remove old debug files first
    if keepTempFiles:
        for filename in [
                matchFilenameOutput, indxFilenameOutput, indxXyFilenameOutput,
                corrFilenameOutput, axyFilenameOutput, logFilenameOutput,
                objsBaseOutput + '_xy2.jpg', objsBaseOutput + '_xy4.jpg',
                objsBaseOutput + '_xy.jpg', objsBaseOutput + '_s.jpg',
                axyBaseOutput + 'xy2.axy', axyBaseOutput + 'xy4.axy',
                axyBaseOutput + 'xy.axy', axyBaseOutput + 's.axy',
                logBaseOutput + 'xy2.log', logBaseOutput + 'xy4.log',
                logBaseOutput + 'xy.log', logBaseOutput + 's.log'
        ]:
            path = os.path.join(debugOutputFolder, filename)
            if os.path.exists(path):
                os.remove(path)

    t0 = time.time()

    def _copyTempFiles(fitsWcsHeader, name):
        if not keepTempFiles:
            return
        if os.path.exists(objsPath):
            p = os.path.join(debugOutputFolder,
                             objsBaseOutput + '_' + name + '.jpg')
            saveImage(p, loadImage(objsPath))  # convert png to jpg

        if not fitsWcsHeader and os.path.exists(axyPath):
            p = os.path.join(debugOutputFolder, axyBaseOutput + name + '.axy')
            shutil.copy(axyPath, p)

        if not fitsWcsHeader and os.path.exists(logPath):
            p = os.path.join(debugOutputFolder, logBaseOutput + name + '.log')
            shutil.copy(logPath, p)

    # first try astrometry.net's own star extraction (simplexy) with different downsampling options

    if arcsecRange:
        if len(arcsecRange) != 2:
            raise ValueError('arcsecRange must be a pair (low,high)')
        arcsecLowHigh = arcsecRange
    else:
        arcsecLowHigh = estimateArcSecRange(imagePath, imageSize)
    _solve = partial(_solveStarfield,
                     imageMaskedPath,
                     tmpDirAstrometry,
                     keepTempFiles=keepTempFiles,
                     timeout=solveTimeout,
                     sigma=sigma,
                     plotsBgImagePath=imagePath,
                     noPlots=noAstrometryPlots,
                     arcsecPerPxLowHigh=arcsecLowHigh,
                     pixelError=pixelError,
                     oddsToSolve=oddsToSolve,
                     astrometryBinPath=astrometryBinPath,
                     useModifiedPath=useModifiedPath,
                     verbose=verbose)

    # "The downsampling just seems to generally do a better job of source detection on
    #  most of the images we get, at least at about 2-4. I think it has to do with either
    #  the PSF size (default is too narrow) or saturation (downsampling smears out
    #  saturated pixels, making them not so saturated)." (Dustin Lang)
    # (https://groups.google.com/d/msg/astrometry/Pp_MZD6s4w8/muuH-1T_zpAJ)
    fitsWcsHeader = None
    if 'xy2' in parameters:
        fitsWcsHeader = _solve(useSextractor=False, downsample=2)
        _copyTempFiles(fitsWcsHeader, 'xy2')

    # Astrometry.net might have already tried downsample=4 when downsample=2 was requested
    # in case no stars could be extracted, so it might happen that we repeat this below.
    # We have to try it anyway because with downsample=2 it could have happened that
    # only 1 or 2 stars in the corners were detected after which astrometry didn't repeat
    # downsampling and just failed.
    # There should be an astrometry flag to disable the repeated downsampling,
    # see also https://groups.google.com/d/msg/astrometry/qNOgWTL1pVA/BNIbqkXEM-gJ
    # Note that with repeated downsampling, astrometry re-uses the sigma (noise level)
    # from the original downsampling, so there may be slight differences if
    # astrometry is manually called with downsample=4 because it recomputes sigma
    # based on downsample=4.
    # NOTE: this is the behaviour of the -D flag of image2xy, which cannot be set from solve-field!

    if fitsWcsHeader is None and 'xy4' in parameters:
        if keepTempFiles and os.path.exists(tmpDirAstrometry):
            shutil.rmtree(tmpDirAstrometry)
        fitsWcsHeader = _solve(useSextractor=False, downsample=4)
        _copyTempFiles(fitsWcsHeader, 'xy4')

    # if this didn't work, we try SExtractor for star extraction

    if fitsWcsHeader is None and 's' in parameters:
        if keepTempFiles and os.path.exists(tmpDirAstrometry):
            shutil.rmtree(tmpDirAstrometry)
        fitsWcsHeader = _solve(useSextractor=True, downsample=None)
        _copyTempFiles(fitsWcsHeader, 's')

    # in case the input image has a low resolution (which is not the case for ISS images)
    # we also might have luck with downsampling disabled and using simplexy:

    if fitsWcsHeader is None and 'xy' in parameters:
        if keepTempFiles and os.path.exists(tmpDirAstrometry):
            shutil.rmtree(tmpDirAstrometry)
        fitsWcsHeader = _solve(useSextractor=False, downsample=None)
        _copyTempFiles(fitsWcsHeader, 'xy')

    print('solving:', time.time() - t0, 's')

    if fitsWcsHeader is not None:
        if keepTempFiles and os.path.exists(indxPath):
            p = os.path.join(debugOutputFolder, indxBaseOutput + '.jpg')
            saveImage(p, loadImage(indxPath))  # convert png to jpg

        tempPaths = [matchPath, indxXyPath, axyPath, corrPath, logPath]
        outputFilenames = [
            matchFilenameOutput, indxXyFilenameOutput, axyFilenameOutput,
            corrFilenameOutput, logFilenameOutput
        ]
        if keepTempFiles:
            outputPaths = [
                os.path.join(debugOutputFolder, f) for f in outputFilenames
            ]
            for tempPath, outputPath in zip(tempPaths, outputPaths):
                if os.path.exists(tempPath):
                    shutil.move(tempPath, outputPath)
                else:
                    print(
                        'WARNING!! {} does not exist after successfully solving, but it should!'
                        .format(tempPath),
                        file=sys.stderr)

    shutil.rmtree(tmpDir)

    return fitsWcsHeader