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
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()
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()
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
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)
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)
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)
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)
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