#Creating a plot showing the normalisation arising from a polypar object Licensed under a MIT license - see LICENSE
"""MUSE-PHANGS alignement module. This module can be used to align MUSE
reconstructed images either with each others or using a reference background
image. It spits the results out in a Fits table which can then be used
to process and mosaic Muse PIXTABLES via the MUSE ESO pipeline.
It includes a normalisation factor, an estimate of the background,
as well as any potential rotation. Fine tuning
can be done by hand by the user, using a set of reference plots.
"""
__authors__ = "Eric Emsellem"
__copyright__ = "(c) 2017, ESO + CRAL"
__license__ = "MIT License"
__contact__ = " <eric.emsellem@eso.org>"
# Import general modules
import os
from os.path import join as joinpath
import glob
import copy
# Import Matplotlib
import matplotlib.pyplot as plt
# Import Numpy Scipy
import numpy as np
from scipy.signal import correlate
# Astropy
from astropy import wcs as awcs
from astropy.io import fits as pyfits
from astropy.io import ascii
from astropy.modeling import models, fitting
from astropy.table import Table, Column
from astropy import units as u
# Import mpdaf
from mpdaf.obj import Image, WCS
# Import needed modules from pymusepipe
from . import util_pipe as upipe # noqua: E402
from .config_pipe import mjd_names, date_names, tpl_names
from .config_pipe import dataset_names, iexpo_names
from .config_pipe import default_offset_table, dict_listObject
from .graph_pipe import (plot_polypar, plot_compare_contours,
plot_compare_cuts, plot_compare_diff)
from .util_pipe import my_linear_model, flatclean_image, get_normfactor
try:
import reproject
from reproject import reproject_interp as repro_interp
from reproject import reproject_exact as repro_exact
except ImportError:
upipe.print_warning("If you wish to use reproject, please install "
"it via: pip install reproject.")
# Skimage if you have it
try:
import skimage
from skimage.registration import phase_cross_correlation
from skimage import transform
except ImportError:
upipe.print_warning("If you wish to use skimage for image registration, "
"please install it via "
" pip install scikit-image "
"or conda install scikit-image")
# Spacepylot
try:
import spacepylot as spp
import spacepylot.alignment as sppalign
import spacepylot.plotting as spppl
from spacepylot.alignment_utilities import TranslationTransform
except ImportError:
upipe.print_warning("If you wish to use spacepylot please install it via"
"pip install or conda install or cloning the github version")
[docs]def is_sequence(arg):
"""Test if sequence and return the boolean result
Parameters
----------
arg : input argument
Returns
-------
result: boolean
"""
return (not hasattr(arg, "strip") and
hasattr(arg, "__getitem__") or
hasattr(arg, "__iter__"))
# ================== Default units ======================== #
# Define useful units
default_muse_unit = u.erg / (u.cm * u.cm * u.second * u.AA) * 1.e-20
default_reference_unit = u.microJansky
dict_equivalencies = {"WFI_BB": u.spectral_density(6483.58 * u.AA),
"DUPONT_R": u.spectral_density(6483.58 * u.AA)}
# ================== Useful function ====================== #
[docs]def create_offset_table(image_names, table_folder="",
table_name="dummy_offset_table.fits", overwrite=False):
"""Create an offset list table from a given set of images. It will use
the MJD and DATE as read from the descriptors of the images. The names for
these keywords is stored in the dictionary default_offset_table from
config_pipe.py
Parameters
----------
image_names : list of str
List of image names to be considered. (Default value = [])
table_folder : str
folder of the table (Default value = "")
table_name : str
name of the table to save ['dummy_offset_table.fits']
(Default value = "dummy_offset_table.fits")
overwrite : bool
if the table exists, it will be overwritten if set
to True only. (Default value = False)
overwrite : bool
if the table exists, it will be overwritten if set
to True only. (Default value = False)
Returns
-------
A fits table with the output given name. (Default value = False)
"""
# Check if table exists and see if overwrite is set up
table_fullname = joinpath(table_folder, table_name)
if not overwrite and os.path.isfile(table_fullname):
upipe.print_warning("[create_offset_table] Table {0} "
"already exists".format(table_fullname))
upipe.print_warning("Use overwrite=True if you wish to proceed")
return
nimages = len(image_names)
if nimages == 0:
upipe.print_warning("No image names provided for create_offset_table")
return
# Gather the values of DATE and MJD from the images
date, mjd, tpls, iexpo, dataset = [], [], [], [], []
for ima in image_names:
if not os.path.isfile(ima):
upipe.print_warning("[create_offset] Image {0} does not exists".format(ima))
continue
head = pyfits.getheader(ima)
date.append(head[date_names['image']])
mjd.append(head[mjd_names['image']])
tpls.append(head[tpl_names['image']])
iexpo.append(head[iexpo_names['image']])
dataset.append(head[dataset_names['image']])
nlines = len(date)
# Create and fill the table
offset_table = Table()
for col in default_offset_table:
[name, form, default] = default_offset_table[col]
offset_table[name] = Column([default for i in range(nlines)],
dtype=form)
offset_table[date_names['table']] = date
offset_table[mjd_names['table']] = mjd
offset_table[tpl_names['table']] = tpls
offset_table[iexpo_names['table']] = iexpo
offset_table[dataset_names['table']] = dataset
# Write the table
offset_table.write(table_fullname, overwrite=overwrite)
[docs]def get_conversion_factor(input_unit, output_unit, filter_name="WFI"):
""" Conversion of units from an input one
to an output one
Input
-----
input_unit: astropy unit
Input astropy unit to analyse
output_unit: astropy unit
Astropy unit to compare to input unit.
equivalencies: astropy equivalency
Used in case there is an existing equivalency
to help the conversion
Returns
-------
conversion: astropy unit conversion
"""
if filter_name not in dict_equivalencies:
upipe.print_warning("Didn't find conversion for this filter. "
"Using 1.0 as a conversion factor")
return 1.0
equivalencies = dict_equivalencies[filter_name]
# First testing if the quantities are Quantity
# If not, transform them
if not isinstance(input_unit, u.quantity.Quantity):
if not isinstance(input_unit, (u.core.Unit, u.core.CompositeUnit)):
upipe.print_warning("Input provided unit could not be converted")
upipe.print_warning("Using 1.0 as a conversion factor")
return 1.0
else:
input_unit = input_unit * 1.0
if not isinstance(output_unit, u.quantity.Quantity):
if not isinstance(output_unit, (u.core.Unit, u.core.CompositeUnit)):
upipe.print_warning("Output provided unit could not be converted")
upipe.print_warning("Using 1.0 as a conversion factor")
return 1.0
else:
output_unit = output_unit * 1.0
if not input_unit.unit.is_equivalent(output_unit):
# if not equivalent we try a spectral density equivalence
if not input_unit.unit.is_equivalent(output_unit,
equivalencies=equivalencies):
upipe.print_warning("Provided units for reference "
"and MUSE images are not equivalent")
upipe.print_warning("A conversion factor of 1.0 will thus be used")
return 1.0
else:
return input_unit.unit.to(
output_unit, equivalencies=equivalencies) * input_unit.value
else:
return input_unit.unit.to(output_unit) * input_unit.value
[docs]def arcsec_to_pixel(hdu, xy_arcsec=(0., 0.)):
"""Transform from arcsec to pixel for the muse image
using the hdu to extract the WCS, hence the scaling.
Input
-----
hdu: astropy hdu (fits)
Input hdu which includes a WCS
xy_arcsec: list of 2 floats ([0,0])
Coordinates to transform from arcsec to pixel.
Returns
-------
xpix, ypix: tuple or list of 2 floats
Pixel coordinates
See also: pixel_to_arcsec (align_pipe.py)
"""
# Matrix
input_wcs = awcs.WCS(hdu.header)
scale_matrix = np.linalg.inv(input_wcs.pixel_scale_matrix * 3600.)
# Transformation in Pixels
dels = np.asarray(xy_arcsec)
xpix = np.sum(dels * scale_matrix[0, :])
ypix = np.sum(dels * scale_matrix[1, :])
return xpix, ypix
[docs]def pixel_to_arcsec(hdu, xy_pixel=(0., 0.)):
"""Transform from arcsec to pixel for the muse image
using the hdu to extract the WCS, hence the scaling.
Input
-----
hdu: astropy hdu (fits)
Input hdu which includes a WCS
xy_pixel: tuple or list of 2 floats ((0,0))
Coordinates to transform from pixel to arcsec
Returns
-------
xarc, yarc: 2 floats
Arcseconds coordinates
See also: arcsec_to_pixel (align_pipe.py)
"""
# Matrix
input_wcs = awcs.WCS(hdu.header)
# Transformation in arcsecond
dels = np.asarray(xy_pixel, dtype=np.float64)
xarc = np.sum(dels * input_wcs.pixel_scale_matrix[0, :] * 3600.)
yarc = np.sum(dels * input_wcs.pixel_scale_matrix[1, :] * 3600.)
return xarc, yarc
[docs]def rotate_pixtables(folder="", name_suffix="", list_ifu=None,
angle=0., **kwargs):
"""Will update the derotator angle in each of the 24 pixtables
Using a loop on rotate_pixtable
Will thus update the HIERARCH ESO INS DROT POSANG keyword.
Input
-----
folder: str
name of the folder where the PIXTABLE are
name_suffix: str
name of the suffix to be used on top of PIXTABLE_OBJECT
list_ifu: list[int]
List of Pixtable numbers. If None, will do all 24
angle: float
Angle to rotate (in degrees)
"""
if list_ifu is None:
list_ifu = np.arange(24) + 1
for nifu in list_ifu:
rotate_pixtable(folder=folder, name_suffix=name_suffix, nifu=nifu,
angle=angle, **kwargs)
[docs]def rotate_pixtable(folder="", name_suffix="", nifu=1, angle=0., **kwargs):
"""Rotate a single IFU PIXTABLE_OBJECT
Will thus update the HIERARCH ESO INS DROT POSANG keyword.
Input
-----
folder: str
name of the folder where the PIXTABLE are
name_suffix: str
name of the suffix to be used on top of PIXTABLE_OBJECT
nifu: int
Pixtable number. Default is 1
angle: float
Angle to rotate (in degrees)
"""
angle_keyword = "HIERARCH ESO INS DROT POSANG"
angle_orig_keyword = "{0} ORIG".format(angle_keyword)
pixtable_basename = kwargs.pop("pixtable_basename",
dict_listObject['OBJECT'])
prefix = kwargs.pop("prefix", "")
name_pixtable = "{0}{1}_{2}-{3:02d}.fits".format(prefix, pixtable_basename,
name_suffix, np.int(nifu))
fullname_pixtable = joinpath(folder, name_pixtable)
fakemode = kwargs.pop("fakemode", False)
# Check if table is there
if not os.path.isfile(fullname_pixtable):
upipe.print_error("Input Pixtable {0} does not exist - Aborting".format(
fullname_pixtable))
return
# Continue with updating the table
mypix = pyfits.open(fullname_pixtable, mode='update')
hd = mypix[0].header
if not fakemode and angle != 0.0:
if not angle_orig_keyword in hd:
hd[angle_orig_keyword] = hd[angle_keyword]
hd[angle_keyword] = hd[angle_orig_keyword] + angle
upipe.print_info("Updating INS DROT POSANG for {0}".format(name_pixtable))
mypix.flush()
# Reading the result and printing
print("=== {} === ".format(name_pixtable), end="")
if not angle_orig_keyword in hd:
print("Present Angle [No Change] (deg): {}".format(hd[angle_keyword]))
else:
print("Orig / New / Rotation Angle (deg): {0:8.4f} / {1:8.4f} / {2:8.4f}".format(
hd[angle_orig_keyword],
hd[angle_keyword],
np.float(hd[angle_keyword]) - hd[angle_orig_keyword]))
[docs]def init_plot_optical_flow(opflow):
"""Initialise the optical flow plot using the AlignmentPlotting
Input
-----
opflow: optical flow instance (see spacepylot)
Returns
-------
An optical flow plot instance
"""
# Initialise the plot
return spppl.AlignmentPlotting.from_align_object(opflow)
#################################################################
# ================== END Useful functions ===================== #
#################################################################
# Main alignment Class
[docs]class AlignMuseDataset(object):
"""Class to align MUSE images onto a reference image.
"""
def __init__(self, name_reference,
folder_reference=None,
folder_muse_images=None,
name_muse_images=None,
sel_indices_images=None,
median_window=10,
subim_window=10,
dynamic_range=10,
border=10, hdu_ext=(0, 1),
chunk_size=15,
firstguess="crosscorr",
**kwargs):
"""Initialise the AlignMuseImages class.
Input
-----
name_reference: str
Name of the reference image (fits file)
folder_reference: str [""]
Folder name of the reference image
folder_muse_images: str [""]
Folder name for the input images to compare
name_muse_images: str or list
List of names for the MUSE images (or str if only 1 image)
suffix_muse_images: str
Suffix to be used for muse image names if only a subset should be selected.
filter_name: str
Name of filter to consider when filtering the muse image names
firstguess: str
If "crosscorr", will use cross-correlation to guess the alignment offsets.
If "fits", will use input fits OFFSET table to start with
If set to None explicitly, will put 0's as start offsets.
Default is "crosscorr".
name_offset_table: str
Name of the fits OFFSET table. Only used as input as guess if "firstguess"
is set to "fits". This name will be the default name when saving
the offsets in an OFFSET fits table.
sel_indices_images: list [None]
List of images to select from the given set
median_window: int [10]
Size of window used in median filter to extract features in
cross-correlation. Should be an odd integer
subim_window: int [10]
Size of window for fitting peak of cross-correlation function
dynamic_range: float [10]
Apply an arctan transform to data to suppress values more than
DynamicRange times the median of the image
border: int [10]
Ignore pixels this close to the border in the cross correlation
hdu_ext: tuple or list of 2 floats (0,1)
Number of the extension for the input reference image
and images to align, respectively. This is used to know
where the data lies in the fits file.
chunk_size: int [15]
Size in pixels of the chunk used to bin and compute the
normalisation factors
Other keywords
--------------
threshold_muse: float [0]
Minimum threshold value to consider for the input
images to align
verbose: bool [True]
If True, spits out more verbose output
plot: bool [True]
If True, will provide plots
debug: bool [False]
If True, will provide some info to debug
which will be stored in the python class
as new attributes. Look for self._temp...
use_polynorm: bool [True]
Save the polynomial fitted slope and use as normalisation
factors
convert_units: bool [True]
Use the given units to convert fluxes
ref_unit: astropy unit
Reference image unit
muse_unit: astropy unit
Input MUSE flux unit
minflux_crosscorr: float [0]
Minimum flux to consider when doing the cross-correlation.
"""
# Some input variables for the cross-correlation
self.verbose = kwargs.pop("verbose", True)
self.plot = kwargs.pop("plot", True)
# Using mpdaf or image_registration - Default is False
# as mpdaf does not define the output WCS homogeneously
self.use_mpdaf = kwargs.pop("use_mpdaf", False)
if self.use_mpdaf:
upipe.print_info("Will use mpdaf for image regridding.")
upipe.print_info("WARNING: when using mpdaf, a potential extra rotation \n"
"has to be applied to redefine the reference grid.")
else:
upipe.print_info("Will use image_registration for image regridding.")
# Set of input parameters for the image processing
self.border = np.int(border)
self.chunk_size = np.int(chunk_size)
self.subim_window = subim_window
self.median_window = median_window
self.dynamic_range = dynamic_range
self.name_reference = name_reference
default_folder = os.getcwd()
if folder_reference is None:
folder_reference = default_folder
self.folder_reference = folder_reference
# Debug option
self._debug = kwargs.pop("debug", False)
if self._debug:
upipe.print_warning("In DEBUG Mode [more printing]")
# Backward compatibility - TO BE REMOVED when fixed
self._backward_comp = kwargs.pop("backward_comp", True)
# Check if folder reference exists
if not os.path.isdir(self.folder_reference):
upipe.print_error("Provided folder_reference is "
"not an existing folder")
return
self.name_muse_images = name_muse_images
self.sel_indices_images = sel_indices_images
if folder_muse_images is None:
folder_muse_images = default_folder
self.folder_muse_images = folder_muse_images
# Check if folder muse images exists
if not os.path.isdir(self.folder_muse_images):
upipe.print_error("Provided folder_muse_images is "
"not an existing folder")
return
# Creating the Header-Align folder
header_folder_name = kwargs.pop("header_folder_name", "AlignHeaders")
self.header_folder_name = joinpath(self.folder_muse_images, header_folder_name)
upipe.safely_create_folder(self.header_folder_name, verbose=self.verbose)
# Creating the Figure folder if needed
figures_folder_name = kwargs.pop("figures_folder_name", "AlignFigures")
self.figures_folder_name = joinpath(self.folder_muse_images, figures_folder_name)
upipe.safely_create_folder(self.figures_folder_name, verbose=self.verbose)
# Getting the names
self.save_hdr = kwargs.pop("save_hdr", False)
self.name_musehdr = kwargs.pop("name_musehdr", "muse")
self.name_offmusehdr = kwargs.pop("name_offmusehdr", "offsetmuse")
self.suffix_images = kwargs.pop("suffix_muse_images", "IMAGE_FOV")
self.filter_name = kwargs.pop("filter_name", "Cousins_R")
# Use polynorm or not
self.use_polynorm = kwargs.pop("use_polynorm", True)
# Use rotation angles from the offset table if they exist
self.use_rotangles = kwargs.pop("use_rotangles", True)
if self.use_rotangles:
upipe.print_warning("By default, rotation angles given in initial "
"offset table will be used if they exist. If "
"not, all initial rotation angles will be set to 0.")
# Getting the unit conversions
self.convert_units = kwargs.pop("convert_units", True)
if self.convert_units:
self.ref_unit = kwargs.pop("ref_unit", default_reference_unit)
self.muse_unit = kwargs.pop("muse_unit", default_muse_unit)
self.conversion_factor = get_conversion_factor(self.ref_unit,
self.muse_unit,
self.filter_name)
else:
self.conversion_factor = 1.0
# Initialise the parameters for the first guess
self.phase_corr = kwargs.pop("phase_corr", True)
self.phase_subsamp = kwargs.pop("phase_subsamp", 10)
self.folder_offset_table = kwargs.pop("folder_offset_table",
self.folder_muse_images)
self.folder_output_table = kwargs.pop("folder_output_table",
self.folder_offset_table)
self.name_offset_table = kwargs.pop("name_offset_table", None)
self.minflux_crosscorr = kwargs.pop("minflux_crosscorr", 0.)
# Get the MUSE images
self._get_list_muse_images()
upipe.print_info("{0} MUSE images detected as input".format(
self.nimages))
if self.nimages == 0:
upipe.print_error("No MUSE images detected. Aborted")
return
self.default_threshold_muse = kwargs.pop("threshold_muse", 0)
# Reset! all parameters
self._reset_init_guess_values()
# Initialise the arrays
self._init_alignment_arrays()
# Which extension to be used for the ref and muse images
self.hdu_ext = hdu_ext
# Open the Ref and MUSE image
status_open = self.open_hdu()
if not status_open:
upipe.print_error("Problem in opening frames, please check your input")
return
# Initialise the offsets using the cross-correlation or FITS table
self.init_guess_offset(firstguess=firstguess)
# Now doing the shifts and projections with the guess/input values
for nima in range(self.nimages):
self._apply_alignment_ima(nima)
[docs] def show_norm_factors(self):
"""Print some information about the normalisation factors.
"""
upipe.print_info("Normalisation factors")
upipe.print_info("Image # : InitFluxScale SlopeFit "
"NormFactor Background")
for nima in range(self.nimages):
upipe.print_info("Image {0:03d}: {1:10.6e} {2:10.6e} "
"{3:10.6e} {4:10.6e}".format(
nima,
self.init_flux_scale[nima],
self.ima_polypar[nima].beta[1],
self.ima_norm_factors[nima],
self.ima_background[nima]))
[docs] def show_linearfit_values(self):
"""Print some information about the linearly fitted parameters
pertaining to the normalisation.
"""
upipe.print_info("Normalisation factors")
upipe.print_info("Image # : BackGround Slope")
for nima in range(self.nimages):
upipe.print_info("Image {0:03d}: {1:10.6e} {2:10.6e}".format(
nima,
self.ima_polypar[nima].beta[0],
self.ima_polypar[nima].beta[1]))
def _init_alignment_arrays(self):
"""Initialise all list and image arrays which are needed
for processing the alignment
"""
self.list_offmuse_hdu = [None] * self.nimages
self.list_wcs_offmuse_hdu = [None] * self.nimages
self.list_proj_refhdu = [None] * self.nimages
self.list_wcs_proj_refhdu = [None] * self.nimages
# Initialise the needed arrays for the PCC offsets
self.pcc_off_pixel = np.zeros((self.nimages, 2), dtype=np.float64)
self.pcc_off_arcsec = np.zeros_like(self.pcc_off_pixel)
self.pcc_off_init = [False] * self.nimages
# Initialise the list of optical flows
self.optical_flows = [None] * self.nimages
self.op_plots = [None] * self.nimages
# Initialise the needed arrays for the cross-correlation offsets
self.cross_off_pixel = np.zeros((self.nimages, 2), dtype=np.float64)
self.extra_off_pixel = np.zeros_like(self.cross_off_pixel)
self.cross_off_arcsec = np.zeros_like(self.cross_off_pixel)
self.extra_off_arcsec = np.zeros_like(self.cross_off_pixel)
self.extra_rotangles = np.zeros((self.nimages), dtype=np.float64)
self._diffra_angle = np.zeros_like(self.extra_rotangles)
# Cross normalisation for the images
# This contains the parameters of the linear fit
self.ima_polypar = [None] * self.nimages
# Normalisation factor to be saved or used
self.ima_norm_factors = np.zeros(self.nimages, dtype=np.float64)
self.ima_background = np.zeros_like(self.ima_norm_factors)
self.ima_threshold = np.full_like(self.ima_norm_factors, self.default_threshold_muse)
self._convolve_muse = np.zeros_like(self.ima_norm_factors)
self._convolve_reference = np.zeros_like(self.ima_norm_factors)
# Default lists for date, mjd, tpls of the MUSE images
self.ima_dateobs = [None] * self.nimages
self.ima_mjdobs = [None] * self.nimages
self.ima_tplstart = [None] * self.nimages
self.ima_iexpo = [None] * self.nimages
self.ima_dataset = [None] * self.nimages
[docs] def init_guess_offset(self, **kwargs):
"""Initialise first guess, either from cross-correlation (default)
or from an Offset FITS Table
Input
-----
firstguess: str
If "crosscorr" uses cross-correlation to get the first guess
of the offsets. If "fits" uses the input fits table.
"""
self.firstguess = kwargs.pop("firstguess", "crosscorr")
# Implement the guess
if self.firstguess is None:
upipe.print_info("No initial guess shift: all set to 0")
self.init_off_arcsec = self.cross_off_arcsec * 0.0
self.init_off_pixel = self.cross_off_pixel * 0.0
return
else:
# Forcing crosscorr if not recognised
if self.firstguess not in ["pcc", "crosscorr", "fits"]:
self.firstguess = "crosscorr"
upipe.print_warning("Keyword 'firstguess' not recognised")
upipe.print_warning("Using Cross-Correlation as a first guess for the alignment")
if self.firstguess == "crosscorr":
upipe.print_info("Using cross-correlation as the initial guess")
# find the cross correlation peaks for each image
# This is using zero offset and zero rotation
# as all parameters have been reset above
# New values will be taken out from the cross-correlation or fits table
# just below with the init_guess_offset
self.find_cross_peak_listima()
# Transfer the cross-correlation offsets to the initialisation offsets
self.init_off_arcsec = self.cross_off_arcsec * 1.0
self.init_off_pixel = self.cross_off_pixel * 1.0
elif self.firstguess == "pcc":
# We use PCC from spacepylot to get the first guess here
self.get_shift_from_pcc_listima()
# Transfer the PCC offsets to the initialisation offsets
self.init_off_arcsec = self.pcc_off_arcsec * 1.0
self.init_off_pixel = self.pcc_off_pixel * 1.0
elif self.firstguess == "fits":
# Look for the offset table
exist_table, self.offset_table = self.open_offset_table(
joinpath(self.folder_offset_table, self.name_offset_table))
if exist_table is not True:
upipe.print_warning("Fits initialisation table not found, "
"setting init value to 0")
self._reset_init_guess_values()
return
upipe.print_info("Using input FITS table as initial guess: {0}".format(
self.name_offset_table))
# First get the right indices for the table by comparing MJD_OBS
if mjd_names['table'] not in self.offset_table.columns:
upipe.print_warning("Input table does not contain MJD_OBS column")
self._reset_init_guess_values()
return
self.table_mjdobs = self.offset_table[mjd_names['table']]
# Now finding the right match with the Images
# Warning, needs > numpy 1.15.0
mjd_values, ind_ima, ind_table = np.intersect1d(
self.ima_mjdobs, self.table_mjdobs,
return_indices=True, assume_unique=True)
# Extracting the flux scale from table
nonan_flux_scale_table = np.where(
np.isnan(self.offset_table['FLUX_SCALE']),
1., self.offset_table['FLUX_SCALE'])
# Extracting the rotangle, but only if there
rotangle_exist = ('ROTANGLE' in self.offset_table.columns)
if rotangle_exist:
nonan_rotangle_table = np.where(
np.isnan(self.offset_table['ROTANGLE']),
0., self.offset_table['ROTANGLE'])
else:
upipe.print_warning("Rotation angles not present in offset table. "
"Please use argument 'extra_rotation' "
"in 'run' to force a non zero value.")
# Loop over the images, using MJD
for nima, mjd in enumerate(self.ima_mjdobs):
# Test if mjd is in the set of mjd_values
if mjd in mjd_values:
# Find the index of the value array where mjd
ind = np.nonzero(mjd_values == mjd)[0][0]
self.init_off_arcsec[nima] = [
self.offset_table['RA_OFFSET'][ind_table[ind]] * 3600. \
* np.cos(np.deg2rad(self.list_dec_muse[nima])),
self.offset_table['DEC_OFFSET'][ind_table[ind]] * 3600.]
self.init_flux_scale[nima] = nonan_flux_scale_table[ind_table[ind]]
if rotangle_exist:
self.init_rotangles[nima] = nonan_rotangle_table[ind_table[ind]]
else:
self.init_rotangles[nima] = 0.0
# Otherwise use default values
else:
self.init_flux_scale[nima] = 1.0
self.init_off_arcsec[nima] = [0., 0.]
self.init_rotangles[nima] = 0.0
# Transform into pixel values
self.init_off_pixel[nima] = arcsec_to_pixel(
self.list_muse_hdu[nima],
self.init_off_arcsec[nima])
def _reset_init_guess_values(self):
"""Reset the initial guess to 0. Hidden function as this is only
used internally
"""
self.table_mjdobs = [None] * self.nimages
self.init_off_pixel = np.zeros((self.nimages, 2), dtype=np.float64)
self.init_off_arcsec = np.zeros((self.nimages, 2), dtype=np.float64)
self.init_flux_scale = np.ones(self.nimages, dtype=np.float64)
self.init_rotangles = np.zeros(self.nimages, dtype=np.float64)
[docs] def open_offset_table(self, name_table=None):
"""Read offset table from fits file
Input
-----
name_table: str
Name of the input OFFSET table
Returns
-------
status: None if no table name is given, False if file does not
exist, True if it does
Table: the result of a astropy.Table.read of the fits table
"""
if name_table is None:
if not hasattr(self, "name_table"):
upipe.print_error("No FITS table name provided, Aborting Open")
return None, Table()
if not os.path.isfile(name_table):
upipe.print_warning("FITS Table ({0}) does not "
" exist yet".format(name_table))
return False, Table()
return True, Table.read(name_table)
[docs] def show_offset_fromfits(self, name_table=None):
"""Print offset table from fits file
Input
-----
name_table: str
Name of the input OFFSET table
"""
exist_table, fits_table = self.open_offset_table(name_table)
if exist_table is None:
return
if (('RA_OFFSET' not in fits_table.columns)
or ('DEC_OFFSET' not in fits_table.columns)):
upipe.print_error("Table does not contain 'RA/DEC_OFFSET' "
"columns, Aborting")
return
upipe.print_info("Offset recorded in OFFSET_LIST Table")
upipe.print_info("Total in ARCSEC")
for nima in range(self.nimages):
upipe.print_info("Image {0:03d} - {1}".format(nima, self.list_name_museimages[nima]))
upipe.print_info(" - {0:8.4f} {1:8.4f}".format(
fits_table['RA_OFFSET'][nima] * 3600
* np.cos(np.deg2rad(self.list_dec_muse[nima])),
fits_table['DEC_OFFSET'][nima] * 3600.))
[docs] def print_images_names(self):
"""Print out the names of the images being considered for alignment
"""
upipe.print_info("Image names")
for nima in range(self.nimages):
upipe.print_info("{0:03d} - {1}".format(nima, self.list_name_museimages[nima]))
[docs] def print_offsets_and_norms(self, filename="_temp.txt",
folder_output_file=None, overwrite=True):
"""Save all offsets and norms into filename. By default, file will
be overwritten.
Input
-----
filename: str
Name of file where the output will be written
folder_output_file: str
Name of output folder where the file will be written
overwrite: bool
Default is True
Creates
-------
Ascii file named via the filename input argument
"""
if folder_output_file is None:
folder_output_file = self.folder_output_table
fullname_file = joinpath(folder_output_file, filename)
if os.path.isfile(fullname_file):
if not overwrite:
upipe.print_warning("File exists and overwrite is False. "
"Aborting.")
return
else:
upipe.print_warning("File exists but will be overwritten as"
"overwrite is True.")
toff_arc = self._total_off_arcsec
toff_pix = self._total_off_pixel
trot = self._total_rotangles
data = {'nb': np.arange(self.nimages) + 1,
'name': [name[-29:-5] for name in self.list_name_museimages],
'xarc': toff_arc[:, 0],
'yarc': toff_arc[:, 1],
'xpix': toff_pix[:, 0],
'ypix': toff_pix[:, 1],
'rot': trot,
'norm': self.ima_norm_factors,
'backg': self.ima_background
}
ascii.write(data, fullname_file,
formats={'nb': '%03d', 'name': '%s>26',
'xarc': '%8.4f', 'yarc': '%8.4f',
'xpix': '%8.4f', 'ypix': '%8.4f',
'rot': '%8.4f', 'norm': '%10.6e',
'backg': '%10.6e'
},
format='fixed_width', overwrite=True)
[docs] def show_offsets(self):
"""Print out the offset from the Alignment class
"""
upipe.print_info("#---- Offset recorded so far ----#")
upipe.print_info("# Name OFFSETS |ARCSEC| "
"X Y |PIXEL| X Y |ROT| (DEG)")
for nima in range(self.nimages):
upipe.print_info("{0:03d} -{1:>26} |ARCSEC|{2:8.4f} {3:8.4f} "
" |PIXEL|{4:8.4f} {5:8.4f} |ROT|{6:8.4f}".format(
nima, self.list_name_museimages[nima][-29:-5],
self._total_off_arcsec[nima][0],
self._total_off_arcsec[nima][1],
self._total_off_pixel[nima][0],
self._total_off_pixel[nima][1],
self._total_rotangles[nima]))
[docs] def save_fits_offset_table(self, name_output_table=None,
folder_output_table=None,
overwrite=False, suffix="", save_flux_scale=True,
save_other_params=True):
"""Save the Offsets into a fits Table
Input
-----
folder_output_table: str [None]
Folder of the output table. If None (default) the folder
for the input offset table will be used or alternatively
the folder of the MUSE images.
name_output_table: str [None]
Name of the output fits table. If None (default) it will
use the one given in self.name_output_table
overwrite: bool [False]
If True, overwrite if the file exists
suffix: str [""]
Suffix to be used to add to the input name. This is handy
to just modify the given fits name with a suffix
(e.g., version number).
save_flux_scale: bool [True]
If True, saving the flux in FLUX_SCALE
If False, do not save the flux conversion
save_other_params: bool [True]
If True, saving the background + rotation
If False, do not save these 2 parameters.
Creates
-------
A fits table with the given name (using the suffix if any)
"""
if name_output_table is None:
if self.name_offset_table is None:
name_output_table = "DUMMY_OFFSET_TABLE.fits"
else:
name_output_table = self.name_offset_table
self.suffix = suffix
self.name_output_table = name_output_table.replace(".fits",
"{0}.fits".format(self.suffix))
if folder_output_table is not None:
self.folder_output_table = folder_output_table
exist_table, fits_table = self.open_offset_table(
joinpath(self.folder_output_table, self.name_output_table))
if exist_table is None:
upipe.print_error("Save is aborted")
return
# Checking if overwrite and exist_table do go together
if exist_table and not overwrite:
upipe.print_warning("Table already exists, "
"but overwrite is set to False")
upipe.print_warning("If you wish to overwrite the table {0}, "
"please set overwrite to True".format(
name_output_table))
return
# Check if RA_OFFSET is there
exist_ra_offset = ('RA_OFFSET' in fits_table.columns)
# First save the DATA and MJD references
fits_table[date_names['table']] = self.ima_dateobs
fits_table[mjd_names['table']] = self.ima_mjdobs
fits_table[tpl_names['table']] = self.ima_tplstart
fits_table[iexpo_names['table']] = self.ima_iexpo
fits_table[dataset_names['table']] = self.ima_dataset
# Saving the final values
fits_table['RA_OFFSET'] = self._total_off_arcsec[:, 0] / 3600. \
/ np.cos(np.deg2rad(self.list_dec_muse))
fits_table['DEC_OFFSET'] = self._total_off_arcsec[:, 1] / 3600.
if save_flux_scale:
fits_table['FLUX_SCALE'] = self.ima_norm_factors
else:
fits_table['FLUX_SCALE'] = 1.0
if save_other_params:
fits_table['BACKGROUND'] = self.ima_background
fits_table['ROTANGLE'] = self._total_rotangles
# Deal with RA_OFFSET_ORIG if needed
if exist_ra_offset:
# if RA_OFFSET exists, then check if the ORIG column is there
if 'RA_OFFSET_ORIG' not in fits_table.columns:
fits_table['RA_OFFSET_ORIG'] = fits_table['RA_OFFSET']
fits_table['DEC_OFFSET_ORIG'] = fits_table['DEC_OFFSET']
fits_table['FLUX_SCALE_ORIG'] = fits_table['FLUX_SCALE']
# Finally add the cross-correlation offsets
fits_table['RA_CROSS_OFFSET'] = self.cross_off_arcsec[:, 0] / 3600. \
/ np.cos(np.deg2rad(self.list_dec_muse))
fits_table['DEC_CROSS_OFFSET'] = self.cross_off_arcsec[:, 1] / 3600.
# Writing up
fits_table.write(joinpath(self.folder_output_table, self.name_output_table),
overwrite=overwrite)
self.name_output_table = name_output_table
def _check_nima(self, nima):
test_nima = nima in range(self.nimages)
if self.verbose:
if not test_nima:
upipe.print_error(f"nima={nima} not within the range "
f"allowed by self.nimages ({self.nimages})")
return test_nima
[docs] def offset_and_compare(self, nima=0, extra_pixel=None, extra_arcsec=None,
extra_rotation=None, **kwargs):
"""Run the offset and comparison for a given image number
Input
-----
nima: int
Index of the image to consider
extra_pixel: list of 2 floats
Offsets in X and Y in pixels to add to the existing
guessed offsets
IMPORTANT NOTE: extra_pixel will be considered first
(before extra_arcsec).
extra_arcsec: list of 2 floats
Offsets in X and Y in arcsec to add to the existing
guessed offsets. Ignored if extra_pixel is given or None
extra_rotation: rotation in degrees
Angle to rotate the image (in degrees). Ignore if None
threshold_muse: float [0]
Threshold to consider when plotting the comparison
Additional arguments
--------------------
plot (bool): if True, will plot the comparison
If not used, will use the default self.plot
* flux comparison (1 to 1)
* Map of the flux ratio
* Contours of the two scaled maps
* Cuts of the division between the 2 maps
See also all arguments from self.compare
"""
if not self._check_nima(nima):
return
# Add the offset from user
border = kwargs.get("border", self.border)
chunk_size = kwargs.get("chunk_size", self.chunk_size)
self.apply_extra_offset_ima(nima=nima, extra_arcsec=extra_arcsec,
extra_pixel=extra_pixel,
extra_rotation=extra_rotation,
border=border, chunk_size=chunk_size)
# Compare contours if plot is set to True
self.compare_ima(nima=nima, **kwargs)
def _get_list_muse_images(self):
"""Extract the name of the muse images
and build the list
"""
from pathlib import Path
if self.name_muse_images is None:
set_of_paths = glob.glob("{0}*{1}*.fits".format(
joinpath(self.folder_muse_images,
self.suffix_images), self.filter_name))
self.list_name_museimages = [Path(muse_path).name
for muse_path in set_of_paths]
# Sort alphabetically
self.list_name_museimages.sort()
# Subselection if sel_indices_images is given
if self.sel_indices_images is not None:
if not all([i in np.arange(len(self.list_name_museimages))
for i in self.sel_indices_images]):
upipe.print_warning("Selection list - sel_indices_images "
"- does not match image list")
upipe.print_warning("Ignoring that input sel_indices_images")
else:
newlist = [self.list_name_museimages[nima]
for nima in self.sel_indices_images]
self.list_name_museimages = newlist
# test if 1 or several images
elif isinstance(self.name_muse_images, str):
self.list_name_museimages = [self.name_muse_images]
elif isinstance(self.name_muse_images, list):
self.list_name_museimages = self.name_muse_images
else:
upipe.print_warning("Name of images is not a string or a list, "
"please check input name_muse_images")
self.list_name_museimages = []
# Number of images to deal with
self.nimages = len(self.list_name_museimages)
[docs] def open_hdu(self):
"""Open the HDU of the MUSE and reference images
"""
status_ref = self._open_ref_hdu()
if not status_ref:
upipe.print_error("Problem in opening Reference frame, please check input")
return 0
status_muse = self._open_muse_nhdu()
if not status_muse:
upipe.print_error("Problem in opening MUSE frame, please check input")
return 0
return 1
def _open_muse_nhdu(self):
"""Open the MUSE images hdu
"""
self.list_name_musehdr = ["{0}{1:03d}.hdr".format(
self.name_musehdr, i + 1) for i in range(self.nimages)]
self.list_name_offmusehdr = ["{0}{1:03d}.hdr".format(
self.name_offmusehdr, i + 1) for i in range(self.nimages)]
self.list_hdulist_muse = [pyfits.open(
joinpath(self.folder_muse_images, self.list_name_museimages[i]))
for i in range(self.nimages)]
self.list_muse_hdu = [hdu[self.hdu_ext[1]]
for hdu in self.list_hdulist_muse]
# CHANGE to mpdaf WCS
self.list_wcs_muse = [WCS(hdu[self.hdu_ext[1]].header)
for hdu in self.list_hdulist_muse]
self.list_dec_muse = np.array([muse_wcs.get_crval2()
for muse_wcs in self.list_wcs_muse])
# Getting the orientation angles
self.list_wcs_rotangles = [musewcs.get_rot()
for musewcs in self.list_wcs_muse]
# Filling in the MJD and DATE OBS keywords for the MUSE images
# If not there, will be filled with "None"
for nima, hdu in enumerate(self.list_hdulist_muse):
if date_names['image'] not in hdu[0].header:
self.ima_dateobs[nima] = None
else:
self.ima_dateobs[nima] = hdu[0].header[date_names['image']]
if mjd_names['image'] not in hdu[0].header:
self.ima_mjdobs[nima] = None
else:
self.ima_mjdobs[nima] = hdu[0].header[mjd_names['image']]
if tpl_names['image'] not in hdu[0].header:
self.ima_tplstart[nima] = None
else:
self.ima_tplstart[nima] = hdu[0].header[tpl_names['image']]
if iexpo_names['image'] not in hdu[0].header:
self.ima_iexpo[nima] = None
else:
self.ima_iexpo[nima] = hdu[0].header[iexpo_names['image']]
if dataset_names['image'] in hdu[0].header:
self.ima_dataset[nima] = hdu[0].header[dataset_names['image']]
else:
if self._backward_comp and dataset_names['oldimage'] in hdu[0].header:
self.ima_dataset[nima] = hdu[0].header[dataset_names['oldimage']]
else:
self.ima_dataset[nima] = None
if self.list_muse_hdu[nima].data is None:
return 0
return 1
def _open_ref_hdu(self):
"""Open the reference image hdu
"""
# Open the images
hdulist_reference = pyfits.open(joinpath(self.folder_reference,
self.name_reference))
self.reference_hdu = hdulist_reference[self.hdu_ext[0]]
if self.reference_hdu.data is None:
upipe.print_error("No data found in extension of reference frame")
upipe.print_error("Check your input, "
"or change the extention number in input hdu_ext[0]")
return 0
return 1
[docs] def get_imaref_muse(self, muse_hdu, rotation=0.0, minflux=0.0, **kwargs):
"""Returns the ref image and muse images on the same grid assuming a given
rotation
Input
-----
muse_hdu: HDU
MUSE hdu file
name_musehdr: str
name of the muse hdr to save
rotation: float
Angle in degrees (0).
minflux: float
Minimum flux to prepare the image (0).
Returns
-------
ima_ref, ima_muse: arrays
Reprojected images
Note that the original images are saved in self._temp_input_origmuse and
self._temp_input_origref when debug mode is on (self._debug)
"""
remove_bkg = kwargs.pop("remove_bkg", True)
squeeze = kwargs.pop("squeeze", True)
border = kwargs.pop("border", self.border)
# Save hdr if save_hdr is True
if self.save_hdr :
name_musehdr = kwargs.pop("name_musehdr", "dummy.hdr")
_ = muse_hdu.header.totextfile(joinpath(self.header_folder_name,
name_musehdr), overwrite=True)
# Projecting the reference image onto the MUSE dataset
hdu_target, proj_ref_hdu, diffra_angle = self._align_reference_hdu(muse_hdu,
target_rotation=rotation,
conversion=True)
# Cleaning the images
if minflux is None:
minflux = self.minflux_crosscorr
minflux_ref = minflux / self.conversion_factor
ima_ref = flatclean_image(proj_ref_hdu.data, border, self.dynamic_range, self.median_window,
minflux=minflux_ref, squeeze=squeeze, remove_bkg=remove_bkg)
ima_muse = flatclean_image(muse_hdu.data, border, self.dynamic_range, self.median_window,
minflux=minflux, squeeze=squeeze, remove_bkg=remove_bkg)
if self._debug:
self._temp_input_origmuse = muse_hdu.data * 1.0
self._temp_input_origref = proj_ref_hdu.data * 1.0
return ima_ref, ima_muse
[docs] def get_shift_from_pcc_listima(self, list_nima=None, minflux=None, verbose=False):
"""Run the PCC shift guess on a list of images given by a list
of indices
Input
-----
list_nima: list of indices for images to process
Should be a list. Default is None
and all images are processed
minflux: float [None]
minimum flux to be used in the cross-correlation
Flux below that value will be set to 0.
"""
upipe.print_info("Starting the PCC shift guess for all images")
if list_nima is None:
list_nima = range(self.nimages)
for nima in list_nima:
self.get_shift_from_pcc_ima(nima=nima, minflux=minflux, verbose=verbose)
[docs] def get_shift_from_pcc_ima(self, nima=None, minflux=None, rotation=None, verbose=False):
"""Run the PCC shift guess for image nima
Input
-----
nima: int
Index of image
minflux: float [None]
minimum flux to be used in the cross-correlation
Flux below that value will be set to 0.
rotation: float
If None, will take the init_rotangle. Otherwise it will take the input value
"""
upipe.print_info(f"PCC for image {nima}")
if rotation is None:
rotation = self.init_rotangles[nima]
# Running shift_from_pcc
self.pcc_off_pixel[nima] = self.get_shift_from_pcc(self.list_muse_hdu[nima],
rotation=rotation,
minflux=minflux,
name_musehdr=self.list_name_musehdr[
nima],
verbose=verbose)
self.pcc_off_arcsec[nima] = pixel_to_arcsec(self.list_muse_hdu[nima],
self.pcc_off_pixel[nima])
self.pcc_off_init[nima] = True
[docs] def get_shift_from_pcc(self, muse_hdu, rotation=0.0, minflux=0.0, verbose=False, **kwargs):
"""Find a guess translation using PCC
Input
-----
muse_hdu: HDU
MUSE hdu file
rotation: float
Angle in degrees (0).
minflux: float
Minimum flux to prepare the image (0).
name_musehdr: str
Name of the muse hdr to save. Optional. Only operational if self.save_hdr is True
Returns
-------
xpix_pcc
ypix_pcc x and y pixel coordinates of the cross-correlation peak
"""
ima_ref, ima_muse = self.get_imaref_muse(muse_hdu, rotation, minflux, border=10,
remove_bkg=False, squeeze=False, **kwargs)
if self._debug:
self._temp_input_pccmuse = ima_muse
self._temp_input_pccref = ima_ref
gt = sppalign.AlignTranslationPCC(ima_muse, ima_ref, verbose=verbose)
# Beware that get_translation from spacepylot return y, x
# Hence the ::-1 to reverse it and the minus sign as it is the reverse than done with PCC
initial_guess_shifts = -gt.get_translation(split_image=2)[::-1]
# Note that get_shift_from_pcc returnx X,U while get_translation from spacepylot return Y, X
return initial_guess_shifts
[docs] def run_optical_flow(self, list_nima=None, save_plot=True, use_rotation=True,
verbose=False, **kwargs):
"""Run Optical flow, first with a guess offset and then iterating. The solution
is saved as extra offset in the class, and a op_plot instance is created.
If save_plot is True, it will save a set of default plots
Input
-----
list_nima: list
List of indices. If None, will use the default list of all images
save_plot : bool
Whether to save the optical flow diagnostic plots or not.
use_rotation: bool
True if you wish to have rotation. False otherwise
verbose: bool
"""
upipe.print_info("Run the optical flow for all images in list (indices)")
if list_nima is None:
list_nima = range(self.nimages)
# Use Translation and Rotation or only rotation?
for nima in list_nima:
upipe.print_info(f"------- Optical Flow for Image {nima} -------")
self.run_optical_flow_ima(nima=nima, save_plot=save_plot,
use_rotation=use_rotation,
verbose=verbose, **kwargs)
[docs] def run_optical_flow_ima(self, nima=0, save_plot=True, use_rotation=True, verbose=False, **kwargs):
"""Run Optical flow on image with index nima,
first with a guess offset and then iterating. The solution
is saved as extra offset in the class, and a op_plot instance is created.
If save_plot is True, it will save a set of default plots
Input
-----
nima: int
Image index.
save_plot : bool
Whether to save the optical flow diagnostic plots or not.
"""
self.iterate_on_optical_flow_ima(nima, verbose=verbose, use_rotation=use_rotation, **kwargs)
self.apply_optical_flow_offset_ima(nima)
if save_plot:
plt.ioff()
self.op_plots[nima].red_blue_before_after()
plt.savefig(joinpath(self.figures_folder_name, f"opflow_redblue_{nima:03d}.png"))
self.op_plots[nima].before_after()
plt.savefig(joinpath(self.figures_folder_name, f"opflow_beforeafter_{nima:03d}.png"))
self.op_plots[nima].illustrate_vector_fields()
plt.savefig(joinpath(self.figures_folder_name, f"opflow_vectorfield_{nima:03d}.png"))
self.op_plots[nima].before_after_diff_frac()
plt.savefig(joinpath(self.figures_folder_name, f"opflow_beforeafter_frac_"
f"{nima:03d}.png"))
plt.close('all')
plt.ion()
[docs] def apply_optical_flow_offset_listima(self, list_nima=None):
"""Apply the optical flow offset as extra pixels offsets and rotation
Input
-----
list_nima: list
If None, will be initiliased to the default list of indices
"""
upipe.print_info("Apply the optical flow offset as extra user offset - and rotation")
if list_nima is None:
list_nima = range(self.nimages)
for nima in list_nima:
self.apply_optical_flow_offset_ima(nima=nima)
[docs] def apply_optical_flow_offset_ima(self, nima=0):
"""Transfer the value of the optical flow into the extra pixel
"""
if self.optical_flows[nima] is None:
upipe.print_warning(f"No optical flow class for image {nima} -"
"- run it before applying it")
return
if self.verbose:
upipe.print_info(f"Apply optical flow offset to Image {nima}")
# Now set up the extra needed offsets as the solution from optical flow
# We need to invert Y, X to X, Y (the [::-1]) and use the minus sign
# considering optical flow derives the motion of ref to MUSE
self.apply_extra_offset_ima(nima=nima,
extra_pixel=(-self.init_off_pixel[nima]
- self.optical_flows[nima].translation[::-1]),
extra_rotation=-self.optical_flows[nima].rotation_deg)
# Initialise the plot
self.op_plots[nima] = init_plot_optical_flow(self.optical_flows[nima])
[docs] def iterate_on_optical_flow_ima(self, nima=0, niter=5, verbose=False,
use_rotation=True, **kwargs):
"""Iterate solution using the optical flow guess
Input
-----
nima: int
Index of image to consider
niter: int
Number of iterations
"""
if self.verbose:
upipe.print_info(f"Optical flow with {niter} iterations for Image {nima}")
reset_opf = kwargs.pop("reset_optical_flow", True)
if self.optical_flows[nima] is None or reset_opf:
self.init_optical_flow_ima(nima, verbose=verbose, **kwargs)
if "homography_method" in kwargs:
homography_method = kwargs.pop("homography_method", transform.EuclideanTransform)
else:
if use_rotation:
homography_method = transform.EuclideanTransform
else:
homography_method = TranslationTransform
self.optical_flows[nima].get_iterate_translation_rotation(niter,
homography_method=homography_method)
[docs] def iterate_on_optical_flow_listima(self, list_nima=None, use_rotation=True, **kwargs):
"""Run the iteration for the optical flow on a list of images
given by a list of indices
Input
-----
list_nima: list of indices for images to process
Should be a list. Default is None
and all images are processed
niter: int
Number of iterations. Optional. If not provided, will use the
default in self.iterate_on_optical_flow_ima
"""
upipe.print_info("Iteration the Optical Flow solution on all images")
if list_nima is None:
list_nima = range(self.nimages)
for nima in list_nima:
self.iterate_on_optical_flow_ima(nima=nima, use_rotation=use_rotation, **kwargs)
[docs] def init_optical_flow_listima(self, list_nima=None, **kwargs):
"""Initialise the optical flow on a list of images
given by a list of indices
Input
-----
list_nima: list of indices for images to process
Should be a list. Default is None
and all images are processed
"""
upipe.print_info("Initiliase the Optical Flow on all images")
if list_nima is None:
list_nima = range(self.nimages)
for nima in list_nima:
self.init_optical_flow_ima(nima=nima, **kwargs)
[docs] def init_optical_flow_ima(self, nima=0, minflux=None, force_pcc=False, guess_offset=(0.,0.),
verbose=False, provide_header=True):
"""Initialise the optical flow using the current image with index nima
Input
-----
nima: int
Index of image
minflux: float
Minimum flux to consider
"""
# If forcing PCC by default, we redo it just for the sake of being close
# Otherwise we just take a 0 value
# WARNING: we need to set the offset as Y, X to pass it on optical flow
if force_pcc:
self.get_shift_from_pcc_ima(nima, minflux=minflux, verbose=verbose)
guess_translation = self.pcc_off_pixel[nima][::-1]
else:
guess_translation = guess_offset[::-1]
# Calling optical flow initialisation
if provide_header:
header = copy.copy(self.list_muse_hdu[nima].header)
else:
header = None
muse_ima = copy.copy(self.list_muse_hdu[nima])
self.optical_flows[nima] = self.init_optical_flow(muse_ima,
rotation=self.init_rotangles[nima],
minflux=minflux,
guess_translation=guess_translation,
name_musehdr=self.list_name_musehdr[
nima],
header=header, verbose=verbose)
[docs] def init_optical_flow(self, muse_hdu, rotation=0., minflux=None, guess_translation=[0.,0.],
header=None, verbose=False, **kwargs):
"""Get the optical flow for this hdu
Input
-----
muse_hdu: HDU
Muse HDU input
rotation: float
Input rotation
minflux: float
Minimum flux to consider in the image
name_musehdr: str
Name of hdr in case those are saved (self.save_hdr is True)
"""
# Getting the images. Note that border must be 0 as otherwise you will need to change
# the WCS (header passed to AlignOpticalFlow
ima_ref, ima_muse = self.get_imaref_muse(muse_hdu, rotation, minflux, border=0,
remove_bkg=False, squeeze=False, **kwargs)
if self._debug:
self._temp_input_opmuse = ima_muse * 1.0
self._temp_input_opref = ima_ref * 1.0
# WARNING: we pass on Y, X hence the [::-1] in the guess_translation parameter
return sppalign.AlignOpticalFlow(ima_muse, ima_ref,
guess_translation=guess_translation[::-1],
header=header,
verbose=verbose)
[docs] def find_cross_peak_ima(self, nima=0, minflux=None):
"""Find the cross correlation peak and get the x and y shifts
for a given image, given its index nima
Input
-----
nima: int
Index of the image
minflux: float
Minimum flux for the cross-correlation
"""
upipe.print_info(f"Cross-correlation for image {nima}")
self.cross_off_pixel[nima] = self.find_cross_peak(self.list_muse_hdu[nima],
rotation=self.init_rotangles[nima],
minflux=minflux,
name_musehdr=self.list_name_musehdr[nima])
self.cross_off_arcsec[nima] = pixel_to_arcsec(self.list_muse_hdu[nima],
self.cross_off_pixel[nima])
[docs] def find_cross_peak_listima(self, list_nima=None, minflux=None):
"""Run the cross correlation peaks on all MUSE images
Derive the self.cross_off_pixel/arcsec parameters
Input
-----
list_nima: list
list of indices for images to process Should be a list. Default is None
and all images are processed
minflux: float [None]
minimum flux to be used in the cross-correlation
Flux below that value will be set to 0.
"""
upipe.print_info("Starting the cross-correlation for all images")
if list_nima is None:
list_nima = range(self.nimages)
for nima in list_nima:
self.find_cross_peak_ima(nima, minflux=minflux)
[docs] def find_cross_peak(self, muse_hdu, rotation=0.0, minflux=None, **kwargs):
"""Aligns the MUSE HDU to a reference HDU
Input
-----
muse_hdu: astropy.io.fits hdu
MUSE hdu file
name_musehdr: str
name of the muse hdr to save
rotation: float
Angle in degrees (0).
minflux: minimum flux to be used in the cross-correlation
Flux below that value will be set to 0.
Default is 0.
Returns
-------
xpix_cross
ypix_cross: x and y pixel coordinates of the cross-correlation peak
"""
ima_ref, ima_muse = self.get_imaref_muse(muse_hdu, rotation, minflux, **kwargs)
if self.phase_corr:
shifts, shift_errors, phasediff = phase_cross_correlation(ima_ref, ima_muse,
upsample_factor=self.phase_subsamp)
xpix_cross = shifts[1]
ypix_cross = shifts[0]
else:
# Cross-correlate the images
ccor = correlate(ima_ref, ima_muse, mode='full', method='auto')
if self._debug:
self._temp_ima_muse_tocc = ima_muse * 1.0
self._temp_ima_ref_tocc = ima_ref * 1.0
self._temp_cc = ccor * 1.0
# Find peak of cross-correlation
maxy, maxx = np.unravel_index(np.argmax(ccor),
ccor.shape)
# Extract a window around it
window = self.subim_window
y, x = np.ix_(np.arange(-window + maxy, window + 1 + maxy),
np.arange(-window + maxx, window + 1 + maxx))
subim = ccor[y % ccor.shape[0], x % ccor.shape[1]]
subim -= subim.min()
mx = np.max(subim)
smaxy, smaxx = np.unravel_index(np.argmax(subim),
subim.shape)
# Fit a 2D Gaussian to that peak
gauss_init = models.Gaussian2D(amplitude=mx,
x_mean=x[0, smaxx],
y_mean=y[smaxy, 0],
x_stddev=2,
y_stddev=2,
theta=0)
fitter = fitting.LevMarLSQFitter()
params = fitter(gauss_init, x * np.ones_like(y),
y * np.ones_like(x),
subim)
# Update Astrometry
# Beware, the sign was changed here and is now ok
xpix_cross = ccor.shape[1] // 2 - params.x_mean
ypix_cross = ccor.shape[0] // 2 - params.y_mean
return xpix_cross, ypix_cross
[docs] def save_image(self, newfits_name=None, nima=0):
"""Save the newly determined hdu
Input
-----
newfits_name: str
Name of the fits file to be used
nima: int [0]
Index of the image to save
Creates
-------
A new fits file
"""
if hasattr(self, "list_offmuse_hdu"):
if newfits_name is None:
newfits_name = self.list_name_museimages[nima].replace(
".fits", "_shift.fits")
self.list_offmuse_hdu[nima].writeto(newfits_name, overwrite=True)
else:
upipe.print_error("There are not yet any new hdu to save")
def _align_hdu(self, hdu_target=None, hdu_to_align=None, target_rotation=0.0,
to_align_rotation=0.0, conversion=False):
"""Project the reference image onto the MUSE dataset
Hidden function, as only used internally
Input
-----
hdu_target: HDU [None]
Target hdu (on to which to project)
hdu_to_align: HDU [None]
Hdu to be aligned
target_rotation: float [0]
Rotation angle in degrees of the target hdu
to_align_rotation: float [0]
Rotation angle in degrees of the to be aligned hdu
Returns
-------
hdu_repr: HDU
Reprojected HDU. None if nothing is provided
"""
# The mpdaf way to project an image onto another one
# WARNING: the reference image will be converted in flux
if conversion:
conversion_factor = self.conversion_factor
else:
conversion_factor = 1.0
if hdu_target is not None and hdu_to_align is not None:
# Getting the reference image data and WCS
wcs_to_align = WCS(hdr=hdu_to_align.header)
# If there is a rotation of the WCS we remove it
if to_align_rotation != 0.:
wcs_to_align.rotate(-to_align_rotation)
ima_to_align = Image(data=hdu_to_align.data * conversion_factor,
wcs=wcs_to_align)
# Apply differential RA if using MPDAF to fix the reference
# Problem existing when using align_with_image
if self.use_mpdaf:
ra_target = WCS(hdu_target.header).to_header()['CRVAL1']
ra_to_align = ima_to_align.wcs.to_header()['CRVAL1']
dec_to_align = ima_to_align.wcs.to_header()['CRVAL2']
dra = ra_target - ra_to_align
diffang = np.rad2deg(np.arccos(np.cos(np.deg2rad(dra))
* (np.sin(np.deg2rad(dec_to_align))) ** 2
+ (np.cos(np.deg2rad(dec_to_align))) ** 2))
upipe.print_warning(f"Differential angle for this comparison is "
f"{diffang:.4f} (using mpdaf needs that fix)")
else:
diffang = 0.
# Getting the MUSE image data and WCS
wcs_target = WCS(hdr=hdu_target.header)
# Fixing the differential angle when using mpdaf
# For repro, the initial value is correct. For mpdaf it needs
# the correction as the reference RA is different when projecting
# - namely = keeping the original RA as a reference -
fixed_target_rotation = target_rotation - diffang
# Doing the rotation now on the target WCS
if fixed_target_rotation != 0.:
wcs_target.rotate(-fixed_target_rotation)
ima_target = Image(data=np.nan_to_num(hdu_target.data),
wcs=wcs_target)
hdu_target = ima_target.get_data_hdu()
# Aligning the reference image with the MUSE image using mpdaf
# align_with_image
if self.use_mpdaf:
ima_aligned = ima_to_align.align_with_image(ima_target, flux=True)
hdu_aligned = ima_aligned.get_data_hdu()
else:
# Change of area
newinc = ima_target.wcs.get_axis_increments(unit=u.deg)
oldinc = ima_to_align.wcs.get_axis_increments(unit=u.deg)
change_area = np.abs(newinc[0] / oldinc[0]) \
* np.abs(newinc[1] / oldinc[1])
daligned = repro_interp(ima_to_align.get_data_hdu(),
ima_target.get_data_hdu().header,
return_footprint=False)
hdu_aligned = pyfits.PrimaryHDU(daligned * change_area)
else:
hdu_aligned = None
diffang = 0.
print("Warning: please provide target HDU to allow reprojection")
return hdu_target, hdu_aligned, diffang
def _align_reference_hdu(self, hdu_target=None, target_rotation=0.0,
ref_rotation=0.0, conversion=True):
"""Project the reference image onto the MUSE dataset
Hidden function, as only used internally
Input
-----
muse_hdu: HDU [None]
Input hdu
target_rotation: float [0]
Target rotation angle in degrees
ref_rotation: float [0]
Rotation of the reference image
Returns
-------
hdu_repr: HDU
Reprojected HDU. None if nothing is provided
"""
return self._align_hdu(hdu_target=hdu_target,
target_rotation=target_rotation,
to_align_rotation=ref_rotation,
hdu_to_align=self.reference_hdu,
conversion=conversion)
@property
def _total_rotangles(self):
return self.init_rotangles + self.extra_rotangles
@property
def _total_off_pixel(self):
return self.init_off_pixel + self.extra_off_pixel
@property
def _total_off_arcsec(self):
return self.init_off_arcsec + self.extra_off_arcsec
def _set_extra_offset_ima(self, nima=0, extra_pixel=None, extra_arcsec=None,
extra_rotation=None):
"""Add user offset in pixel and transform into arcseconds
Input
-----
extra_pixel: list of 2 floats [0,0]
Extra offsets (x,y) in pixels
extra_arsec: list of 2 floats [0,0]
Extra offsets (x,y) in arcsec if extra_pixel is not provided
extra_rotation: rotation in degrees [0]
nima: int
Index of image to consider
"""
# Add this to the extra_off arrays
if extra_pixel is None:
if extra_arcsec is not None:
# Transforming the arc into pix
self.extra_off_arcsec[nima] = extra_arcsec
# Transforming into pixels - would be better with setter
self.extra_off_pixel[nima] = arcsec_to_pixel(self.list_muse_hdu[nima], extra_arcsec)
else:
self.extra_off_pixel[nima] = extra_pixel
# Transforming into arcsec - would be better with setter
self.extra_off_arcsec[nima] = pixel_to_arcsec(self.list_muse_hdu[nima], extra_pixel)
# And the rotation angle in degrees
if extra_rotation is not None:
self.extra_rotangles[nima] = extra_rotation
def _apply_alignment_ima(self, nima=0, **kwargs):
"""Apply alignment for image with index nima
Input
----
nima: int
Index of image. Default is 0
"""
# Call the alignment given the input nima image
upipe.print_info(f"- Image Offset/Rotation [{nima}] ="
f" {self.list_name_museimages[nima]} -")
hdu_offmuse, hdu_projref, diffra = self._apply_alignment(self.list_muse_hdu[nima],
total_off_pixel=self._total_off_pixel[nima], total_rotangle=self._total_rotangles[nima])
# Creating a new Primary HDU and its WCS with the input data, and the new Header
# This HDU has now been offset using the total offsets
self.list_offmuse_hdu[nima] = hdu_offmuse
self.list_wcs_offmuse_hdu[nima] = WCS(hdu_offmuse.header)
# Saving the projected reference hdu and its WCS
self.list_proj_refhdu[nima] = hdu_projref
self.list_wcs_proj_refhdu[nima] = WCS(hdu_projref.header)
# Writing this up in an ascii file for record purposes
if self.save_hdr:
_ = self.list_offmuse_hdu[nima].header.totextfile(joinpath(self.header_folder_name,
self.list_name_offmusehdr[nima]),
overwrite=True)
# Getting the normalisation factors using those last projections
_, _, self.ima_polypar[nima] = self.get_normfactor_ima(nima=nima, **kwargs)
# Saving the normalisation
self.save_polypar_ima(nima, self.ima_polypar[nima].beta)
def _apply_alignment(self, hdu, total_off_pixel=None, total_off_arcsec=None,
total_rotangle=0.):
"""Create New HDU after shifting it with the right offset
(only considering image with index nima)
Input
-----
hdu : HDU
Input HDU of image to offset
nima: int
Index of image to consider
Does not return anything, but could in principle
"""
# Create a new Header
newhdr = copy.copy(hdu.header)
# Using input offset or total
if total_off_pixel is None:
if total_off_arcsec is None:
upipe.print_warning("No input offset given: using [0., 0.]")
total_off_pixel = [0., 0.]
else:
total_off_pixel = arcsec_to_pixel(hdu, total_off_arcsec)
else:
total_off_arcsec = pixel_to_arcsec(hdu, total_off_pixel)
# Shift the HDU in X and Y
if self.verbose:
upipe.print_info(f"Offset [PIXELS]: {total_off_pixel[0]:8.4f}"
f" {total_off_pixel[1]:8.4f} / "
f"[ARCSEC]: {total_off_arcsec[0]:8.4f}"
f"{total_off_arcsec[1]:8.4f}")
upipe.print_info(f"Rotation [DEGREE]: {total_rotangle:8.4f}")
# Shifting the CRPIX values in the header
newhdr['CRPIX1'] += total_off_pixel[0]
newhdr['CRPIX2'] += total_off_pixel[1]
# Creating a new Primary HDU with the input data, and the new Header
# This HDU has now been offset using the total offsets
hdu_offmuse = pyfits.PrimaryHDU(hdu.data, header=newhdr)
# Reprojecting the Reference image onto the new MUSE frame
hdu_target, hdu_projref, diffra = \
self._align_reference_hdu(hdu_target=hdu_offmuse, target_rotation=total_rotangle)
return hdu_offmuse, hdu_projref, diffra
[docs] def get_normfactor_ima(self, nima=0, median_filter=True, border=0,
convolve_muse=0., convolve_reference=0., chunk_size=10):
"""Get the normalisation factor for shifted and projected images. This function only
consider the input image given by index nima and the reference image (after
projection).
Input
-----
nima: int
Index of image to consider
median_filter: bool
If True, will median filter
convolve_muse: float [0]
Will convolve the image with index nima
with a gaussian with that sigma. 0 means no convolution
convolve_reference: float [0]
Will convolve the reference image
with a gaussian with that sigma. 0 means no convolution
border: int
Number of pixels to crop
threshold_muse: float [None]
Threshold for the input image flux to consider
chunk_size: int
Size of chunks to consider for chunk statistics (polynomial normalisation)
Returns
-------
data: 2d array
refdata: 2d array
The 2 arrays (input, reference) after processing
"""
return get_normfactor(self.list_offmuse_hdu[nima].data, self.list_proj_refhdu[nima].data,
convolve_data1=convolve_muse, convolve_data2=convolve_reference,
median_filter=median_filter, border=border,
chunk_size=chunk_size, threshold=self.ima_threshold[nima])
[docs] def save_polypar_ima(self, nima=0, beta=None):
"""Saving the input values into the fixed arrays for the polynomial
Input
-----
beta: list/array of 2 floats
"""
if beta is not None:
self.ima_background[nima] = beta[0]
self.ima_norm_factors[nima] = beta[1]
[docs] def compare_ima(self, nima=0, nima_museref=None,
convolve_muse=0, convolve_reference=0., **kwargs):
"""
Input
-----
nima: int
Index of input image
nima_museref: int
Index of second input image for the reference. Default is None, hence ignored
and the default reference image will be used.
Create
------
Plots which compare the two input datasets as defined by the indices
"""
# Getting the first MUSE image (aligned)
musehdu = self.list_offmuse_hdu[nima]
if self._debug:
self._temp_prealigned = self.list_muse_hdu[nima].data
# Getting data from the MUSE ref image if one is given
museref = nima_museref is not None
if museref:
refhdu = self.list_offmuse_hdu[nima_museref]
else:
refhdu = self.list_proj_refhdu[nima]
threshold_muse = kwargs.pop("threshold_muse", self.ima_threshold[nima])
musedata, refdata, polypar = get_normfactor(musehdu.data, refhdu.data,
convolve_data1=convolve_muse,
convolve_data2=convolve_reference,
threshold=threshold_muse)
self.compare(data1=musedata, data2=refdata, header=musehdu.header,
suffix_fig=f"{nima:03d}", **kwargs)
[docs] def compare(self, data1, data2, header=None,
start_nfig=1, nlevels=10, levels=None, convolve_data1=0., convolve_data2=0.,
showcontours=True, showcuts=True, shownormalise=True, showdiff=True,
normalise=True, median_filter=True, ncuts=5, percentage=5.,
suffix_fig="", **kwargs):
"""Compare the projected reference and MUSE image
by plotting the contours, the difference and vertical/horizontal cuts.
Parameters
----------
data1:
data2: 2d np.arrays
Array to compare
header: Header
If provided, will be use to get the WCS in the plots. Default is None (ignored).
polypar: ODR result
If None, it will be recalculated
showcontours: bool [True]
showcuts: bool [True]
shownormalise: bool [True]
showdiff: bool [True]
All options corresponding to 1 specific plot. By default
show them all (all True)
ncuts: int [5]
Number of vertical / horizontal cuts along the ratio
between the 2 maps to be shown ("cuts")
percentage: float [5]
Used to compute which percentile to show
start_nfig: int [1]
Number of the matplotlib Figure to start with
nlevels: int [10]
Number of levels for the contour plots
levels: list of float [None]
Specific list of levels if any (default is None)
convolve_data1: float [0]
If not 0, will convolve with a gaussian of that sigma
convolve_data2: float [0]
If not 0, will convolve the reference image
with a gaussian of that sigma
savefig (bool): False
If True, will save the figure into a png
suffix_fig: str
Suffix name to add to the figure filenames
Makes a maximum of 4 figures
"""
border = kwargs.pop("border", self.border)
chunk_size = kwargs.pop("chunk_size", self.chunk_size)
savefig = kwargs.pop("savefig", True)
threshold = kwargs.pop("threshold", 0.)
# Getting the data
_, _, polypar = get_normfactor(data1, data2,
convolve_data1=convolve_data1,
convolve_data2=convolve_data2,
median_filter=median_filter, border=border,
chunk_size=chunk_size,
threshold=threshold)
# If normalising, use the polypar slope and background
if normalise:
if self.verbose:
upipe.print_info(f"Renormalising the data as: Normalised = "
f"{polypar.beta[1]:8.4e} * ({polypar.beta[0]:8.4e} + MUSE)")
data1 = my_linear_model(polypar.beta, data1)
# Save the frames in case this is needed
if self._debug:
self._temp_aligned = data1
self._temp_reference = data2
# WCS for plotting using astropy
if header is not None:
plotwcs = awcs.WCS(header)
else:
plotwcs = None
# Preparing the figure
current_fig = start_nfig
self.list_figures = []
foldfig = self.figures_folder_name
if suffix_fig != "":
suffix_fig = f"_{suffix_fig}"
# Starting the plotting
if shownormalise:
plot_polypar(polypar, labels=["MuseData", "RefData"], figfolder=foldfig,
fignum=current_fig, namefig=f"align_norm_scatter{suffix_fig}.png",
savefig=savefig)
self.list_figures.append(current_fig)
current_fig += 1
if showcontours:
plot_compare_contours(data1, data2, plotwcs=plotwcs, fignum=current_fig,
labels=['MUSE', 'REF'], nlevels=nlevels, levels=levels,
figfolder=foldfig, namefig=f"align_contours{suffix_fig}.png",
title=f'Image {suffix_fig}')
self.list_figures.append(current_fig)
current_fig += 1
if showcuts:
plot_compare_cuts(data1, data2, ncuts=ncuts, fignum=current_fig, figfolder=foldfig,
namefig=f"align_cuts{suffix_fig}.png", savefig=savefig)
self.list_figures.append(current_fig)
current_fig += 1
if showdiff:
plot_compare_diff(data1, data2, plotwcs=plotwcs, fignum=current_fig,
figfolder=foldfig, percentage=percentage,
namefig=f"align_diff{suffix_fig}.png", savefig=savefig)
self.list_figures.append(current_fig)
current_fig += 1