예제 #1
0
def correct_raw_file(input_fname, *, output, bias_fname, flat_fname='', overscan=50, overwrite=True, mode='spec'):
    """
    Wrapper for `raw_correction` using file input instead of image input

    Returns
    -------
    output_msg : string
        Log of status messages
    """
    hdr = get_alfosc_header(input_fname)
    sci_raw = fits.getdata(input_fname)
    msg = "          - Loaded input image: %s" % input_fname

    output_msg = raw_correction(sci_raw, hdr, bias_fname, flat_fname, output=output,
                                overscan=overscan, overwrite=overwrite, mode=mode)
    output_msg = msg + '\n' + output_msg

    return output_msg
예제 #2
0
def verify_arc_frame(arc_fname, dispaxis=2):
    """
    Return True if the arc lines fill the full detector plane, else return False.
    The full detector plane is considered the region between 10th and 90th percentile
    of the pixels along the slit.

    `dispaxis` = 2 for vertical spectra, 1 for horizontal spectra.
    """
    arc2D = fits.getdata(arc_fname)
    hdr = get_alfosc_header(arc_fname)
    if 'DISPAXIS' in hdr:
        dispaxis = hdr['DISPAXIS']

    if dispaxis == 2:
        # Reorient image to have dispersion along x-axis:
        arc2D = arc2D.T

    ilow, ihigh = detect_borders(arc2D)
    imax = arc2D.shape[0]
    result = (ilow < 0.1 * imax) & (ihigh > 0.9 * imax)
    return result
예제 #3
0
def detect_filter_edge(fname, overscan=50):
    """Automatically detect edges in the normalized flat field"""
    # Get median profile along slit:
    img = fits.getdata(fname)
    hdr = get_alfosc_header(fname)
    if 'OVERSCAN' in hdr:
        overscan = 0

    # Using the normalized flat field, the values are between 0 and 1.
    # Convert the image to a binary mask image:
    img[img > 0.5] = 1.
    img[img < 0.5] = -1.

    fx = np.median(img, 0) > 0.5
    fy = np.median(img, 1) > 0.5

    # Detect initial edges:
    x1 = np.min(fx.nonzero()[0])
    x2 = np.max(fx.nonzero()[0])

    y1 = np.min(fy.nonzero()[0])
    y2 = np.max(fy.nonzero()[0])

    # If the edge is curved, decrease the trim edges
    # until they are fully inside the image region
    lower = img[y1, x1]
    upper = img[y2, x2]
    while lower < 0 and upper < 0:
        x1 += 1
        y1 += 1
        x2 -= 1
        y2 -= 1
        lower = img[y1, x1]
        upper = img[y2, x2]

    return (x1-overscan, x2-overscan, y1, y2)
예제 #4
0
def calculate_response(raw_fname,
                       *,
                       arc_fname,
                       pixtable_fname,
                       bias_fname,
                       flat_fname,
                       output='',
                       output_dir='',
                       pdf_fname='',
                       order=3,
                       smoothing=0.02,
                       interactive=False,
                       dispaxis=2,
                       order_wl=4,
                       order_bg=5,
                       rectify_options={},
                       app=None):
    """
    Extract and wavelength calibrate the standard star spectrum.
    Calculate the instrumental response function and fit the median filtered data points
    with a Chebyshev polynomium of the given *order*.

    Parameters
    ==========
    raw_fname : string
        File name for the standard star frame

    arc_fname : string
        File name for the associated arc frame

    pixtable_fname : string
        Filename of pixel table for the identified lines in the arc frame

    bias_fname : string
        Master bias file name to subtract bias level.
        If nothing is given, no bias level correction is performed.

    flat_fname : string
        Normalized flat file name.
        If nothing is given, no spectral flat field correction is performed.

    output : string  [default='']
        Output filename of the response function FITS Table

    output_dir : string  [default='']
        Output directory for the response function and intermediate files.
        Filenames are autogenerated from OBJECT name and GRISM

    pdf_fname : string  [default='']
        Output filename for diagnostic plots.
        If none, autogenerate from OBJECT name

    order : integer  [default=8]
        Order of the spline interpolation of the response function

    smoothing : float  [default=0.02]
        Smoothing factor for spline interpolation

    interactive : boolean  [default=False]
        Interactively subtract background and extract 1D spectrum
        using a graphical interface. Otherwise, automatically identify
        object, subtract background and extract object.

    dispaxis : integer  [default=2]
        Dispersion axis. 1: horizontal spectra, 2: vertical spectra (default for most ALFOSC grisms)

    order_wl : integer  [default=4]
        Polynomial order for wavelength solution as function of pixel value (from `identify`)

    rectify_options : dict()  [default={}]
        Dictionary of keyword arguments for `rectify`

    Returns
    =======
    response_output : string
        Filename of resulting response function

    output_msg : string
        Log of the function call
    """
    msg = list()

    hdr = get_alfosc_header(raw_fname)
    raw2D = fits.getdata(raw_fname)
    msg.append("          - Loaded flux standard image: %s" % raw_fname)

    # Setup the filenames:
    grism = alfosc.grism_translate[hdr['ALGRNM']]
    star = hdr['TCSTGT']
    # Check if the star name is in the header:
    star = alfosc.lookup_std_star(star)
    if star is None:
        msg.append(
            "[WARNING] - No reference data found for the star %s (TCS Target Name)"
            % hdr['TCSTGT'])
        msg.append(
            "[WARNING] - The reduced spectra will not be flux calibrated")
        output_msg = "\n".join(msg)
        return None, output_msg

    response_output = 'response_%s_%s.fits' % (star, grism)
    std_tmp_fname = 'std_corr2D_%s.fits' % star
    rect2d_fname = 'std_rect2D_%s.fits' % star
    bgsub2d_fname = 'std_bgsub2D_%s.fits' % star
    ext1d_output = 'std_ext1D_%s.fits' % star
    extract_pdf_fname = 'std_ext1D_diagnostics.pdf'
    if output_dir:
        response_output = os.path.join(output_dir, response_output)
        std_tmp_fname = os.path.join(output_dir, std_tmp_fname)
        rect2d_fname = os.path.join(output_dir, rect2d_fname)
        bgsub2d_fname = os.path.join(output_dir, bgsub2d_fname)
        ext1d_output = os.path.join(output_dir, ext1d_output)
        extract_pdf_fname = os.path.join(output_dir, extract_pdf_fname)

    try:
        output_msg = raw_correction(raw2D,
                                    hdr,
                                    bias_fname,
                                    flat_fname,
                                    output=std_tmp_fname,
                                    overwrite=True,
                                    overscan=50)
        msg.append(output_msg)
    except:
        msg.append("Unexpected error: %r" % sys.exc_info()[0])
        output_msg = "\n".join(msg)
        raise Exception(output_msg)

    # Rectify 2D image and wavelength calibrate:
    try:
        rectify_options['plot'] = False
        rect_msg = rectify(std_tmp_fname,
                           arc_fname,
                           pixtable_fname,
                           output=rect2d_fname,
                           dispaxis=dispaxis,
                           order_wl=order_wl,
                           **rectify_options)
        msg.append(rect_msg)
    except:
        msg.append("Unexpected error: %r" % sys.exc_info()[0])
        output_msg = "\n".join(msg)
        raise Exception(output_msg)
    # After RECTIFY all images are oriented with the dispersion axis horizontally

    # Subtract background:
    try:
        bg_msg = auto_fit_background(rect2d_fname,
                                     bgsub2d_fname,
                                     dispaxis=1,
                                     order_bg=order_bg,
                                     plot_fname='',
                                     kappa=100,
                                     fwhm_scale=5)
        msg.append(bg_msg)
    except:
        msg.append("Unexpected error: %r" % sys.exc_info()[0])
        output_msg = "\n".join(msg)
        raise Exception(output_msg)

    # Extract 1-dimensional spectrum:
    if interactive:
        try:
            msg.append(
                "          - Starting Graphical User Interface for Spectral Extraction"
            )
            extract_gui.run_gui(bgsub2d_fname,
                                output_fname=ext1d_output,
                                app=app,
                                order_center=5,
                                order_width=5,
                                smoothing=smoothing,
                                dx=20)
            msg.append(" [OUTPUT] - Writing fits table: %s" % ext1d_output)
        except:
            msg.append("Unexpected error: %r" % sys.exc_info()[0])
            output_msg = "\n".join(msg)
            raise Exception(output_msg)
    else:
        try:
            ext_msg = auto_extract(bgsub2d_fname,
                                   ext1d_output,
                                   dispaxis=1,
                                   N=1,
                                   pdf_fname=extract_pdf_fname,
                                   model_name='moffat',
                                   dx=20,
                                   order_center=4,
                                   order_width=5,
                                   xmin=20,
                                   ymin=5,
                                   ymax=-5,
                                   kappa_cen=5.,
                                   w_cen=15)
            msg.append(ext_msg)
        except:
            msg.append("Unexpected error: %r" % sys.exc_info()[0])
            output_msg = "\n".join(msg)
            raise Exception(output_msg)

    # Load the 1D extraction:
    wl, ext1d = load_spectrum1d(ext1d_output)
    cdelt = np.mean(np.diff(wl))

    # Load the spectroscopic standard table:
    # The files are located in 'calib/std/'
    star_name = alfosc.standard_star_names[star]
    std_tab = np.loadtxt(alfosc.path + '/calib/std/%s.dat' % star_name.lower())
    msg.append("          - Loaded reference data for object: %s" % star_name)

    # Calculate the flux in the pass bands:
    msg.append("          - Calculating flux in reference band passes")
    wl0 = list()
    flux0 = list()
    mag = list()
    for l0, m0, b in std_tab:
        l1 = l0 - b / 2.
        l2 = l0 + b / 2.
        band = (wl >= l1) * (wl <= l2)
        if np.sum(band) > 3:
            f0 = np.nanmean(ext1d[band])
            if f0 > 0:
                flux0.append(f0)
                wl0.append(l0)
                mag.append(m0)
    wl0 = np.array(wl0)
    flux0 = np.array(flux0)
    mag = np.array(mag)

    # Median filter the points:
    msg.append(
        "          - Median filter and smooth the data points to remove outliers"
    )
    med_flux_tab = median_filter(flux0, 5)
    med_flux_tab = gaussian_filter1d(med_flux_tab, 1)
    noise = mad(flux0 - med_flux_tab) * 1.5
    good = np.abs(flux0 - med_flux_tab) < 2 * noise
    good[:3] = True
    good[-3:] = True

    # Load extinction table:
    msg.append("          - Loaded the average extinction data for La Palma")
    wl_ext, A0 = np.loadtxt(alfosc.path + '/calib/lapalma.ext', unpack=True)
    ext = np.interp(wl0, wl_ext, A0)
    exptime = hdr['EXPTIME']
    airmass = hdr['AIRMASS']
    msg.append("          - AIRMASS: %.2f" % airmass)
    msg.append("          - EXPTIME: %.1f" % exptime)

    # Convert AB magnitudes to fluxes (F-lambda):
    F = 10**(-(mag + 2.406) / 2.5) / (wl0)**2

    # Calculate Sensitivity:
    C = 2.5 * np.log10(flux0 / (exptime * cdelt * F)) + airmass * ext

    if interactive:
        msg.append("          - ")
        msg.append("          - Starting Graphical User Interface...")
        try:
            response = response_gui.run_gui(ext1d_output,
                                            response_output,
                                            order=3,
                                            smoothing=0.02,
                                            app=app)
            msg.append(
                " [OUTPUT] - Saving the response function as FITS table: %s" %
                response_output)
        except:
            msg.append("Unexpected error: %r" % sys.exc_info()[0])
            output_msg = "\n".join(msg)
            raise Exception(output_msg)
    else:
        # Fit a smooth polynomium to the calculated response:
        msg.append(
            "          - Interpolating the filtered response curve data points"
        )
        msg.append("          - Spline degree: %i" % order)
        msg.append("          - Smoothing factor: %.3f" % smoothing)
        response_fit = UnivariateSpline(wl0[good],
                                        C[good],
                                        k=order,
                                        s=smoothing)
        # response_fit = Chebyshev.fit(wl0[good], C[good], order, domain=[wl.min(), wl.max()])
        response = response_fit(wl)

    # -- Prepare PDF figure:
    if not pdf_fname:
        pdf_fname = 'response_diagnostic_' + hdr['OBJECT'] + '.pdf'
        pdf_fname = os.path.join(output_dir, pdf_fname)
    pdf = backend_pdf.PdfPages(pdf_fname)

    # Plot the extracted spectrum
    fig1 = plt.figure()
    ax = fig1.add_subplot(111)
    ax.plot(wl, ext1d)
    ax.set_ylim(ymin=0.)
    power = np.floor(np.log10(np.nanmax(ext1d))) - 1
    majFormatter = ticker.FuncFormatter(lambda x, p: my_formatter(x, p, power))
    ax.get_yaxis().set_major_formatter(majFormatter)
    ax.set_ylabel(u'Counts  [$10^{{{0:d}}}$ ADU]'.format(int(power)),
                  fontsize=14)
    ax.set_xlabel(u"Wavelength  [Å]", fontsize=14)
    ax.set_title(u"Filename: %s  ,  Star: %s" % (raw_fname, star.upper()))
    pdf.savefig(fig1)

    # Plot the response function:
    fig2 = plt.figure()
    ax2 = fig2.add_subplot(111)
    ax2.plot(wl0, C, color='RoyalBlue', marker='o', ls='')
    ax2.plot(wl0[~good], C[~good], color='r', marker='o', ls='')
    ax2.set_ylabel(u"Response  ($F_{\\lambda}$)", fontsize=14)
    ax2.set_xlabel(u"Wavelength  (Å)", fontsize=14)
    ax2.set_title(u"Response function, grism: " + hdr['ALGRNM'])
    ax2.plot(wl, response, color='crimson', lw=1)
    pdf.savefig(fig2)
    pdf.close()
    msg.append(" [OUTPUT] - Saving the response function diagnostics:  %s" %
               pdf_fname)

    if interactive:
        # The GUI saved the output already...
        pass
    else:
        # --- Prepare FITS output:
        resp_hdr = fits.Header()
        resp_hdr['GRISM'] = grism
        resp_hdr['OBJECT'] = hdr['OBJECT']
        resp_hdr['DATE-OBS'] = hdr['DATE-OBS']
        resp_hdr['EXPTIME'] = hdr['EXPTIME']
        resp_hdr['AIRMASS'] = hdr['AIRMASS']
        resp_hdr['ALGRNM'] = hdr['ALGRNM']
        resp_hdr['ALAPRTNM'] = hdr['ALAPRTNM']
        resp_hdr['RA'] = hdr['RA']
        resp_hdr['DEC'] = hdr['DEC']
        resp_hdr['COMMENT'] = 'PyNOT response function'
        resp_hdr['AUTHOR'] = 'PyNOT version %s' % __version__
        prim = fits.PrimaryHDU(header=resp_hdr)
        col_wl = fits.Column(name='WAVE',
                             array=wl,
                             format='D',
                             unit='Angstrom')
        col_resp = fits.Column(name='RESPONSE',
                               array=response,
                               format='D',
                               unit='-2.5*log(erg/s/cm2/A)')
        tab = fits.BinTableHDU.from_columns([col_wl, col_resp])
        hdu = fits.HDUList()
        hdu.append(prim)
        hdu.append(tab)
        hdu.writeto(response_output, overwrite=True)
        msg.append(
            " [OUTPUT] - Saving the response function as FITS table: %s" %
            response_output)
    msg.append("")
    output_msg = "\n".join(msg)
    return response_output, output_msg
예제 #5
0
def raw_correction(sci_raw, hdr, bias_fname, flat_fname='', output='', overscan=50, overwrite=True, mode='spec'):
    """
    Perform bias subtraction, flat field correction, and cosmic ray rejection

    Parameters
    ==========

    sci_raw : np.array (M, N)
        Input science image to reduce

    hdr : FITS Header
        Header associated with the science image

    bias_fname : string
        Filename of bias image to subtract from `sci_raw`

    flat_fname : string  [default='']
        Filename of normalized flat field image. If none is given, no flat field correction will be applied

    output : string  [default='']
        Output filename

    overscan : int  [default=50]
        Number of pixels in overscan at the edge of the CCD.
        The overscan region will be trimmed.

    overwrite : boolean  [default=True]
        Overwrite existing output file if True.

    Returns
    -------
    output_msg : string
        Log of status messages
    """
    msg = list()
    mbias = fits.getdata(bias_fname)
    bias_hdr = get_alfosc_header(bias_fname)
    msg.append("          - Loaded bias image: %s" % bias_fname)
    if flat_fname:
        mflat = fits.getdata(flat_fname)
        mflat[mflat == 0] = 1
        flat_hdr = get_alfosc_header(flat_fname)
        msg.append("          - Loaded flat field image: %s" % flat_fname)
    else:
        mflat = 1.
        msg.append("          - Not flat field image provided. No correction applied!")

    # Trim overscan of raw image:
    # - Trimming again of processed images doesn't change anything,
    # - so do it just in case the input has not been trimmed
    sci_raw, hdr = trim_overscan(sci_raw, hdr, overscan=overscan, mode=mode)
    mbias, bias_hdr = trim_overscan(mbias, bias_hdr, overscan=overscan, mode=mode)
    mflat, flat_hdr = trim_overscan(mflat, flat_hdr, overscan=overscan, mode=mode)

    # Correct image:
    sci = (sci_raw - mbias)/mflat

    # Calculate error image:
    if hdr['CCDNAME'] == 'CCD14':
        hdr['GAIN'] = 0.16
    gain = hdr['GAIN']
    readnoise = hdr['RDNOISE']
    with warnings.catch_warnings():
        warnings.simplefilter('ignore')
        err = np.sqrt(gain*sci + readnoise**2) / gain
    msg.append("          - Created noise image")
    msg.append("          - Gain=%.2f  and Read Noise=%.2f" % (gain, readnoise))

    # Fix NaN values from negative pixel values:
    err_NaN = np.isnan(err)
    err[err_NaN] = readnoise/gain
    msg.append("          - Correcting NaNs in noise image: %i pixel(s)" % np.sum(err_NaN))
    hdr['DATAMIN'] = np.nanmin(sci)
    hdr['DATAMAX'] = np.nanmax(sci)
    hdr['EXTNAME'] = 'DATA'
    hdr['AUTHOR'] = 'PyNOT version %s' % __version__
    hdr['OVERSCAN'] = 'TRIMMED'

    mask = np.zeros_like(sci, dtype=int)
    msg.append("          - Empty pixel mask created")
    mask_hdr = fits.Header()
    mask_hdr.add_comment("0 = Good Pixels")
    mask_hdr.add_comment("1 = Cosmic Ray Hits")
    for key in ['CRPIX1', 'CRPIX2', 'CRVAL1', 'CRVAL2', 'CTYPE1', 'CTYPE2', 'CUNIT1', 'CUNIT2']:
        mask_hdr[key] = hdr[key]

    if mode == 'spec':
        mask_hdr['CDELT1'] = hdr['CDELT1']
        mask_hdr['CDELT2'] = hdr['CDELT2']
    else:
        mask_hdr['CD1_1'] = hdr['CD1_1']
        mask_hdr['CD1_2'] = hdr['CD1_2']
        mask_hdr['CD2_1'] = hdr['CD2_1']
        mask_hdr['CD2_2'] = hdr['CD2_2']

    sci_ext = fits.PrimaryHDU(sci, header=hdr)
    err_ext = fits.ImageHDU(err, header=hdr, name='ERR')
    mask_ext = fits.ImageHDU(mask, header=mask_hdr, name='MASK')
    output_HDU = fits.HDUList([sci_ext, err_ext, mask_ext])
    output_HDU.writeto(output, overwrite=overwrite)
    msg.append("          - Successfully corrected the image.")
    msg.append(" [OUTPUT] - Saving output: %s" % output)
    msg.append("")
    output_msg = "\n".join(msg)

    return output_msg
예제 #6
0
def auto_fit_background(data_fname, output_fname, dispaxis=2, order_bg=3, kappa=10, fwhm_scale=3, xmin=0, xmax=None, plot_fname=''):
    """
    Fit background in 2D spectral data. The background is fitted along the spatial rows by a Chebyshev polynomium.

    Parameters
    ==========
    data_fname : string
        Filename of the FITS image to process

    output_fname : string
        Filename of the output FITS image containing the background subtracted image
        as well as the background model in a separate extension.

    dispaxis : integer  [default=1]
        Dispersion axis, 1: horizontal spectra, 2: vertical spectra
        The function does not rotate the final image, only the intermediate image
        since it's faster to operate on rows than on columns.

    order_bg : integer  [default=3]
        Order of the Chebyshev polynomium to fit the background

    xmin, xmax : integer  [default=0, None]
        Mask out pixels below xmin and above xmax

    fwhm_scale : float  [default=3]
        Number of FWHM below and above centroid of auto-detected trace
        that will be masked out during fitting.

    kappa : float  [default=10]
        Threshold for masking out cosmic rays etc.

    plot_fname : string  [default='']
        Filename of diagnostic plots. If nothing is given, do not plot.

    Returns
    =======
    output_fname : string
        Background model of the 2D frame, same shape as input data.

    output_msg : string
        Log of messages from the function call
    """
    msg = list()
    data = fits.getdata(data_fname)
    hdr = get_alfosc_header(data_fname)
    if 'DISPAXIS' in hdr:
        dispaxis = hdr['DISPAXIS']

    if dispaxis == 1:
        # transpose the horizontal spectra to make them vertical
        # since it's faster to fit rows than columns
        data = data.T
    msg.append("          - Loaded input image: %s" % data_fname)

    msg.append("          - Fitting background along the spatial axis with polynomium of order: %i" % order_bg)
    msg.append("          - Automatic masking of outlying pixels and object trace")
    bg2D = fit_background_image(data, order_bg=order_bg, kappa=kappa, fwhm_scale=fwhm_scale, xmin=xmin, xmax=xmax)

    if plot_fname:
        fig2D = plt.figure()
        ax1_2d = fig2D.add_subplot(121)
        ax2_2d = fig2D.add_subplot(122)
        noise = mad(data)
        v1 = np.median(data) - 5*noise
        v2 = np.median(data) + 5*noise
        ax1_2d.imshow(data, origin='lower', vmin=v1, vmax=v2)
        ax1_2d.set_title("Input Image")
        ax2_2d.imshow(data-bg2D, origin='lower', vmin=v1, vmax=v2)
        ax2_2d.set_title("Background Subtracted")
        ax1_2d.set_xlabel("Spatial Axis  [pixels]")
        ax2_2d.set_xlabel("Spatial Axis  [pixels]")
        ax1_2d.set_ylabel("Dispersion Axis  [pixels]")
        fig2D.tight_layout()
        fig2D.savefig(plot_fname)
        plt.close()
        msg.append(" [OUTPUT] - Saving diagnostic figure: %s" % plot_fname)

    data = data - bg2D
    if dispaxis == 1:
        # Rotate the model and data back to horizontal orientation:
        data = data.T
        bg2D = bg2D.T

    with fits.open(data_fname) as hdu:
        hdu[0].data = data
        sky_hdr = fits.Header()
        sky_hdr['BUNIT'] = 'count'
        copy_keywords = ['CRPIX1', 'CRVAL1', 'CDELT1', 'CTYPE1', 'CUNIT1']
        copy_keywords += ['CRPIX2', 'CRVAL2', 'CDELT2']
        sky_hdr['CTYPE2'] = 'LINEAR'
        sky_hdr['CUNIT2'] = 'Pixel'
        for key in copy_keywords:
            sky_hdr[key] = hdr[key]
        sky_hdr['AUTHOR'] = 'PyNOT version %s' % __version__
        sky_hdr['ORDER'] = (order_bg, "Polynomial order along spatial rows")
        sky_ext = fits.ImageHDU(bg2D, header=sky_hdr, name='SKY')
        hdu.append(sky_ext)
        hdu.writeto(output_fname, overwrite=True)

    msg.append(" [OUTPUT] - Saving background subtracted image: %s" % output_fname)
    msg.append("")
    output_msg = "\n".join(msg)
    return output_msg
예제 #7
0
def rectify(img_fname,
            arc_fname,
            pixtable_fname,
            output='',
            fig_dir='',
            order_bg=5,
            order_2d=5,
            order_wl=4,
            log=False,
            N_out=None,
            interpolate=True,
            binning=1,
            dispaxis=2,
            fit_window=20,
            plot=True,
            overwrite=True,
            verbose=False,
            overscan=50):

    msg = list()
    arc2D = fits.getdata(arc_fname)
    arc_hdr = get_alfosc_header(arc_fname)
    arc2D, arc_hdr = trim_overscan(arc2D, arc_hdr, overscan=overscan)
    img2D = fits.getdata(img_fname)
    msg.append("          - Loaded image: %s" % img_fname)
    msg.append("          - Loaded reference arc image: %s" % arc_fname)
    try:
        err2D = fits.getdata(img_fname, 'ERR')
        msg.append("          - Loaded error image")
    except KeyError:
        err2D = None
    try:
        mask2D = fits.getdata(img_fname, 'MASK')
        msg.append("          - Loaded mask image")
    except KeyError:
        mask2D = np.zeros_like(img2D)
    hdr = fits.getheader(img_fname)

    ref_table = np.loadtxt(pixtable_fname)
    msg.append("          - Loaded reference pixel table: %s" % pixtable_fname)
    if 'DISPAXIS' in hdr.keys():
        dispaxis = hdr['DISPAXIS']

    if dispaxis == 2:
        msg.append(
            "          - Rotating frame to have dispersion along x-axis")
        # Reorient image to have dispersion along x-axis:
        arc2D = arc2D.T
        img2D = img2D.T
        mask2D = mask2D.T
        if err2D is not None:
            err2D = err2D.T
        pix_in = create_pixel_array(hdr, dispaxis=2)

        if 'DETYBIN' in hdr:
            binning = hdr['DETYBIN']
            hdr['DETYBIN'] = hdr['DETXBIN']
            hdr['DETXBIN'] = binning
        hdr['CDELT2'] = hdr['CDELT1']
        hdr['CRVAL2'] = hdr['CRVAL1']
        hdr['CRPIX2'] = hdr['CRPIX1']
        hdr['CTYPE2'] = 'LINEAR'
        hdr['CUNIT2'] = hdr['CUNIT1']
    else:
        pix_in = create_pixel_array(hdr, dispaxis=1)
        if 'DETXBIN' in hdr:
            binning = hdr['DETXBIN']

    ilow, ihigh = detect_borders(arc2D)
    msg.append("          - Image shape: (%i, %i)" % arc2D.shape)
    msg.append("          - Detecting arc line borders: %i -- %i" %
               (ilow, ihigh))
    hdr['CRPIX2'] += ilow
    # Trim images:
    arc2D = arc2D[ilow:ihigh, :]
    img2D = img2D[ilow:ihigh, :]
    err2D = err2D[ilow:ihigh, :]
    mask2D = mask2D[ilow:ihigh, :]

    msg.append("          - Subtracting arc line continuum background")
    msg.append("          - Polynomial order of 1D background: %i" % order_bg)
    arc2D_sub, _ = subtract_arc_background(arc2D, deg=order_bg)

    msg.append("          - Fitting arc line positions within %i pixels" %
               fit_window)
    msg.append("          - Number of lines to fit: %i" % ref_table.shape[0])
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        pixtab2d = create_2d_pixtab(arc2D_sub,
                                    pix_in,
                                    ref_table,
                                    dx=fit_window)

    msg.append(
        "          - Constructing 2D wavelength grid with polynomial order: %i"
        % order_2d)
    fit_table2d = fit_2dwave_solution(pixtab2d, deg=order_2d)

    msg.append(
        "          - Residuals of arc line positions relative to fitted 2D grid:"
    )
    fit_residuals = format_table2D_residuals(pixtab2d, fit_table2d, ref_table)

    msg.append("              Wavelength    Arc Residual   Max. Curvature")
    for l0, line_residual, line_minmax in fit_residuals:
        msg.append("              %10.3f    %-12.3f   %-14.3f" %
                   (l0, line_residual, line_minmax))

    msg.append(
        "          - Interpolating input image onto rectified wavelength solution"
    )
    transform_output = apply_transform(img2D,
                                       pix_in,
                                       fit_table2d,
                                       ref_table,
                                       err2D=err2D,
                                       mask2D=mask2D,
                                       header=hdr,
                                       order_wl=order_wl,
                                       log=log,
                                       N_out=N_out,
                                       interpolate=interpolate)
    img2D_corr, err2D_corr, mask2D, wl, hdr_corr, trans_msg = transform_output
    msg.append(trans_msg)
    hdr.add_comment('PyNOT version %s' % __version__)
    if plot:
        plot_fname = os.path.join(fig_dir, 'PixTable2D.pdf')
        plot_2d_pixtable(arc2D_sub,
                         pix_in,
                         pixtab2d,
                         fit_table2d,
                         filename=plot_fname)
        msg.append(
            "          - Plotting fitted arc line positions in 2D frame")
        msg.append(" [OUTPUT] - Saving figure: %s" % plot_fname)

    if output:
        if output[-5:] != '.fits':
            output += '.fits'
    else:
        object_name = hdr['OBJECT']
        output = 'RECT2D_%s.fits' % (object_name)

    hdr_corr['DISPAXIS'] = 1
    hdr_corr['EXTNAME'] = 'DATA'
    with fits.open(img_fname) as hdu:
        hdu[0].data = img2D_corr
        hdu[0].header = hdr_corr
        if err2D_corr is not None:
            hdu['ERR'].data = err2D_corr
            err_hdr = hdu['ERR'].header
            copy_keywords = ['CRPIX1', 'CRVAL1', 'CDELT1', 'CTYPE1', 'CUNIT1']
            copy_keywords += ['CRPIX2', 'CRVAL2', 'CDELT2', 'CTYPE2', 'CUNIT2']
            for key in copy_keywords:
                err_hdr[key] = hdr[key]
            hdu['ERR'].header = err_hdr
        if 'MASK' in hdu:
            hdu['MASK'].data = mask2D
            mask_hdr = hdu['MASK'].header
            copy_keywords = ['CRPIX1', 'CRVAL1', 'CDELT1', 'CTYPE1', 'CUNIT1']
            copy_keywords += ['CRPIX2', 'CRVAL2', 'CDELT2', 'CTYPE2', 'CUNIT2']
            for key in copy_keywords:
                mask_hdr[key] = hdr[key]
            hdu['MASK'].header = mask_hdr
        else:
            mask_hdr = fits.Header()
            mask_hdr.add_comment("2 = Good Pixels")
            mask_hdr.add_comment("1 = Cosmic Ray Hits")
            mask_hdr['AUTHOR'] = 'PyNOT version %s' % __version__
            copy_keywords = ['CRPIX1', 'CRVAL1', 'CDELT1', 'CTYPE1', 'CUNIT1']
            copy_keywords += ['CRPIX2', 'CRVAL2', 'CDELT2', 'CTYPE2', 'CUNIT2']
            for key in copy_keywords:
                mask_hdr[key] = hdr[key]
            mask_ext = fits.ImageHDU(mask2D, header=mask_hdr, name='MASK')
            hdu.append(mask_ext)
        hdu.writeto(output, overwrite=overwrite)
    msg.append(" [OUTPUT] - Saving rectified 2D image: %s" % output)
    msg.append("")
    output_str = "\n".join(msg)
    if verbose:
        print(output_str)
    return output_str