Пример #1
0
def _ellip_smooth(R, E, deg):
    model = make_pipeline(PolynomialFeatures(deg), HuberRegressor(epsilon=2.))
    model.fit(np.log10(R).reshape(-1, 1), _inv_x_to_eps(E))
    return _x_to_eps(model.predict(np.log10(R).reshape(-1, 1)))
Пример #2
0
def Isophote_Initialize(IMG, results, options):
    """
    Determine the global pa and ellipticity for a galaxy. First grow circular isophotes
    until reaching near the noise floor, then evaluate the phase of the second FFT
    coefficients and determine the average direction. Then fit an ellipticity for one
    of the outer isophotes.
    """

    ######################################################################
    # Initial attempt to find size of galaxy in image
    # based on when isophotes SB values start to get
    # close to the background noise level
    circ_ellipse_radii = [results['psf fwhm']]
    allphase = []
    dat = IMG - results['background']

    while circ_ellipse_radii[-1] < (len(IMG) / 2):
        circ_ellipse_radii.append(circ_ellipse_radii[-1] * (1 + 0.2))
        isovals = _iso_extract(dat,
                               circ_ellipse_radii[-1],
                               0.,
                               0.,
                               results['center'],
                               more=True,
                               sigmaclip=True,
                               sclip_nsigma=3,
                               interp_mask=True)
        coefs = fft(isovals[0])
        allphase.append(coefs[2])
        # Stop when at 3 time background noise
        if np.quantile(isovals[0], 0.8) < (3 * results['background noise']
                                           ) and len(circ_ellipse_radii) > 4:
            break
    logging.info('%s: init scale: %f pix' %
                 (options['ap_name'], circ_ellipse_radii[-1]))
    # Find global position angle.
    phase = (-Angle_Median(np.angle(allphase[-5:])) / 2) % np.pi

    # Find global ellipticity
    test_ellip = np.linspace(0.05, 0.95, 15)
    test_f2 = []
    for e in test_ellip:
        test_f2.append(
            sum(
                list(
                    _fitEllip_loss(e, dat, circ_ellipse_radii[-2] *
                                   m, phase, results['center'],
                                   results['background noise'])
                    for m in np.linspace(0.8, 1.2, 5))))
    ellip = test_ellip[np.argmin(test_f2)]
    res = minimize(lambda e, d, r, p, c, n: sum(
        list(
            _fitEllip_loss(_x_to_eps(e[0]), d, r * m, p, c, n)
            for m in np.linspace(0.8, 1.2, 5))),
                   x0=_inv_x_to_eps(ellip),
                   args=(dat, circ_ellipse_radii[-2], phase, results['center'],
                         results['background noise']),
                   method='Nelder-Mead',
                   options={
                       'initial_simplex': [[_inv_x_to_eps(ellip) - 1 / 15],
                                           [_inv_x_to_eps(ellip) + 1 / 15]]
                   })
    if res.success:
        logging.debug(
            '%s: using optimal ellipticity %.3f over grid ellipticity %.3f' %
            (options['ap_name'], _x_to_eps(res.x[0]), ellip))
        ellip = _x_to_eps(res.x[0])

    # Compute the error on the parameters
    ######################################################################
    RR = np.linspace(circ_ellipse_radii[-2] - results['psf fwhm'],
                     circ_ellipse_radii[-2] + results['psf fwhm'], 10)
    errallphase = []
    for rr in RR:
        isovals = _iso_extract(dat,
                               rr,
                               0.,
                               0.,
                               results['center'],
                               more=True,
                               sigmaclip=True,
                               sclip_nsigma=3,
                               interp_mask=True)
        coefs = fft(isovals[0])
        errallphase.append(coefs[2])
    sample_pas = (-np.angle(1j * np.array(errallphase) / np.mean(errallphase))
                  / 2) % np.pi
    pa_err = iqr(sample_pas, rng=[16, 84]) / 2
    res_multi = map(
        lambda rrp: minimize(lambda e, d, r, p, c, n: _fitEllip_loss(
            _x_to_eps(e[0]), d, r, p, c, n),
                             x0=_inv_x_to_eps(ellip),
                             args=(dat, rrp[0], rrp[1], results['center'],
                                   results['background noise']),
                             method='Nelder-Mead',
                             options={
                                 'initial_simplex': [[
                                     _inv_x_to_eps(ellip) - 1 / 15
                                 ], [_inv_x_to_eps(ellip) + 1 / 15]]
                             }), zip(RR, sample_pas))
    ellip_err = iqr(list(_x_to_eps(rm.x[0])
                         for rm in res_multi), rng=[16, 84]) / 2

    circ_ellipse_radii = np.array(circ_ellipse_radii)

    if 'ap_doplot' in options and options['ap_doplot']:

        ranges = [
            [
                max(0,
                    int(results['center']['x'] -
                        circ_ellipse_radii[-1] * 1.5)),
                min(dat.shape[1],
                    int(results['center']['x'] + circ_ellipse_radii[-1] * 1.5))
            ],
            [
                max(0,
                    int(results['center']['y'] -
                        circ_ellipse_radii[-1] * 1.5)),
                min(dat.shape[0],
                    int(results['center']['y'] + circ_ellipse_radii[-1] * 1.5))
            ]
        ]

        LSBImage(dat[ranges[1][0]:ranges[1][1], ranges[0][0]:ranges[0][1]],
                 results['background noise'])
        # plt.imshow(np.clip(dat[ranges[1][0]: ranges[1][1], ranges[0][0]: ranges[0][1]],a_min = 0, a_max = None),
        #            origin = 'lower', cmap = 'Greys_r', norm = ImageNormalize(stretch=LogStretch()))
        plt.gca().add_patch(
            Ellipse((results['center']['x'] - ranges[0][0],
                     results['center']['y'] - ranges[1][0]),
                    2 * circ_ellipse_radii[-1],
                    2 * circ_ellipse_radii[-1] * (1. - ellip),
                    phase * 180 / np.pi,
                    fill=False,
                    linewidth=1,
                    color='y'))
        plt.plot([results['center']['x'] - ranges[0][0]],
                 [results['center']['y'] - ranges[1][0]],
                 marker='x',
                 markersize=3,
                 color='r')
        if not ('ap_nologo' in options and options['ap_nologo']):
            AddLogo(plt.gcf())
        plt.savefig(
            '%sinitialize_ellipse_%s.jpg' %
            (options['ap_plotpath'] if 'ap_plotpath' in options else '',
             options['ap_name']),
            dpi=options['ap_plotdpi'] if 'ap_plotdpi' in options else 300)
        plt.close()

        fig, ax = plt.subplots(2, 1, figsize=(6, 6))
        plt.subplots_adjust(hspace=0.01, wspace=0.01)
        ax[0].plot(circ_ellipse_radii[:-1],
                   ((-np.angle(allphase) / 2) % np.pi) * 180 / np.pi,
                   color='k')
        ax[0].axhline(phase * 180 / np.pi, color='r')
        ax[0].axhline((phase + pa_err) * 180 / np.pi,
                      color='r',
                      linestyle='--')
        ax[0].axhline((phase - pa_err) * 180 / np.pi,
                      color='r',
                      linestyle='--')
        #ax[0].axvline(circ_ellipse_radii[-2], color = 'orange', linestyle = '--')
        ax[0].set_xlabel('Radius [pix]', fontsize=16)
        ax[0].set_ylabel('FFT$_{1}$ phase [deg]', fontsize=16)
        ax[0].tick_params(labelsize=12)
        ax[1].plot(test_ellip, test_f2, color='k')
        ax[1].axvline(ellip, color='r')
        ax[1].axvline(ellip + ellip_err, color='r', linestyle='--')
        ax[1].axvline(ellip - ellip_err, color='r', linestyle='--')
        ax[1].set_xlabel('Ellipticity [1 - b/a]', fontsize=16)
        ax[1].set_ylabel('Loss [FFT$_{2}$/med(flux)]', fontsize=16)
        ax[1].tick_params(labelsize=14)
        plt.tight_layout()
        if not ('ap_nologo' in options and options['ap_nologo']):
            AddLogo(plt.gcf())
        plt.savefig(
            '%sinitialize_ellipse_optimize_%s.jpg' %
            (options['ap_plotpath'] if 'ap_plotpath' in options else '',
             options['ap_name']),
            dpi=options['ap_plotdpi'] if 'ap_plotdpi' in options else 300)
        plt.close()

    auxmessage = 'global ellipticity: %.3f +- %.3f, pa: %.3f +- %.3f deg, size: %f pix' % (
        ellip, ellip_err, PA_shift_convention(phase) * 180 / np.pi,
        pa_err * 180 / np.pi, circ_ellipse_radii[-2])
    return IMG, {
        'init ellip': ellip,
        'init ellip_err': ellip_err,
        'init pa': phase,
        'init pa_err': pa_err,
        'init R': circ_ellipse_radii[-2],
        'auxfile initialize': auxmessage
    }
Пример #3
0
def Isophote_Extract(IMG, results, options):
    """
    Extract isophotes given output profile from Isophotes_Simultaneous_Fit which
    parametrizes pa and ellipticity via functions which map all the reals to the
    appropriate parameter range. This function also extrapolates the profile to
    large and small radii (simply by taking the parameters at the edge, no
    functional extrapolation). By default uses a linear radius growth, however
    for large images, it uses a geometric radius growth of 10% per isophote.
    """
    use_center = results['center']

    # Radius values to evaluate isophotes
    R = [
        options['ap_sampleinitR'] if 'ap_sampleinitR' in options else min(
            1., results['psf fwhm'] / 2)
    ]
    while (((R[-1] < options['ap_sampleendR'] if 'ap_sampleendR' in options
             else True) and R[-1] < 3 * results['fit R'][-1]) or
           (options['ap_extractfull'] if 'ap_extractfull' in options else
            False)) and R[-1] < max(IMG.shape) / np.sqrt(2):
        if 'ap_samplestyle' in options and options[
                'ap_samplestyle'] == 'geometric-linear':
            if len(R) > 1 and abs(R[-1] - R[-2]) >= (
                    options['ap_samplelinearscale'] if 'ap_samplelinearscale'
                    in options else 3 * results['psf fwhm']):
                R.append(R[-1] + (
                    options['ap_samplelinearscale'] if 'ap_samplelinearscale'
                    in options else results['psf fwhm']))
            else:
                R.append(R[-1] *
                         (1. +
                          (options['ap_samplegeometricscale']
                           if 'ap_samplegeometricscale' in options else 0.1)))
        elif 'ap_samplestyle' in options and options[
                'ap_samplestyle'] == 'linear':
            R.append(R[-1] +
                     (options['ap_samplelinearscale'] if 'ap_samplelinearscale'
                      in options else 0.5 * results['psf fwhm']))
        else:
            R.append(R[-1] *
                     (1. + (options['ap_samplegeometricscale']
                            if 'ap_samplegeometricscale' in options else 0.1)))
    R = np.array(R)
    logging.info('%s: R complete in range [%.1f,%.1f]' %
                 (options['ap_name'], R[0], R[-1]))

    # Interpolate profile values, when extrapolating just take last point
    E = _x_to_eps(
        np.interp(R, results['fit R'], _inv_x_to_eps(results['fit ellip'])))
    E[R < results['fit R'][0]] = results['fit ellip'][
        0]  #R[R < results['fit R'][0]] * results['fit ellip'][0] / results['fit R'][0]
    E[R < results['psf fwhm']] = R[R < results['psf fwhm']] * results[
        'fit ellip'][0] / results['psf fwhm']
    E[R > results['fit R'][-1]] = results['fit ellip'][-1]
    tmp_pa_s = np.interp(R, results['fit R'], np.sin(2 * results['fit pa']))
    tmp_pa_c = np.interp(R, results['fit R'], np.cos(2 * results['fit pa']))
    PA = _x_to_pa(
        ((np.arctan(tmp_pa_s / tmp_pa_c) + (np.pi * (tmp_pa_c < 0))) %
         (2 * np.pi)) /
        2)  #_x_to_pa(np.interp(R, results['fit R'], results['fit pa']))
    PA[R < results['fit R'][0]] = _x_to_pa(results['fit pa'][0])
    PA[R > results['fit R'][-1]] = _x_to_pa(results['fit pa'][-1])

    # Get errors for pa and ellip
    if 'fit ellip_err' in results and (not results['fit ellip_err'] is None
                                       ) and 'fit pa_err' in results and (
                                           not results['fit pa_err'] is None):
        Ee = np.clip(np.interp(R, results['fit R'], results['fit ellip_err']),
                     a_min=1e-3,
                     a_max=None)
        Ee[R < results['fit R'][0]] = results['fit ellip_err'][0]
        Ee[R > results['fit R'][-1]] = results['fit ellip_err'][-1]
        PAe = np.clip(np.interp(R, results['fit R'], results['fit pa_err']),
                      a_min=1e-3,
                      a_max=None)
        PAe[R < results['fit R'][0]] = results['fit pa_err'][0]
        PAe[R > results['fit R'][-1]] = results['fit pa_err'][-1]
    else:
        Ee = np.zeros(len(R))
        PAe = np.zeros(len(R))

    return IMG, _Generate_Profile(IMG, results, R, E, Ee, PA, PAe, options)
Пример #4
0
def Isophote_Fit_FFT_mean(IMG, results, options):
    """
    Fit isophotes by minimizing the amplitude of the second FFT coefficient, relative to the local median flux.
    Included is a regularization term which penalizes isophotes for having large differences between parameters
    of adjacent isophotes.
    """

    if 'ap_scale' in options:
        scale = options['ap_scale']
    else:
        scale = 0.2

    # subtract background from image during processing
    dat = IMG - results['background']
    mask = results['mask'] if 'mask' in results else None
    if not np.any(mask):
        mask = None

    # Determine sampling radii
    ######################################################################
    shrink = 0
    while shrink < 5:
        sample_radii = [3 * results['psf fwhm'] / 2]
        while sample_radii[-1] < (max(IMG.shape) / 2):
            isovals = _iso_extract(dat,
                                   sample_radii[-1],
                                   results['init ellip'],
                                   results['init pa'],
                                   results['center'],
                                   more=False,
                                   mask=mask)
            if np.mean(isovals) < (options['ap_fit_limit']
                                   if 'ap_fit_limit' in options else
                                   1) * results['background noise']:
                break
            sample_radii.append(sample_radii[-1] * (1. + scale /
                                                    (1. + shrink)))
        if len(sample_radii) < 15:
            shrink += 1
        else:
            break
    if shrink >= 5:
        raise Exception(
            'Unable to initialize ellipse fit, check diagnostic plots. Possible missed center.'
        )
    ellip = np.ones(len(sample_radii)) * results['init ellip']
    pa = np.ones(len(sample_radii)) * results['init pa']
    logging.debug('%s: sample radii: %s' %
                  (options['ap_name'], str(sample_radii)))

    # Fit isophotes
    ######################################################################
    perturb_scale = np.array([0.03, 0.06])
    regularize_scale = options[
        'ap_regularize_scale'] if 'ap_regularize_scale' in options else 1.
    N_perturb = 5

    count = 0

    count_nochange = 0
    use_center = copy(results['center'])
    I = np.array(range(len(sample_radii)))
    while count < 300 and count_nochange < (3 * len(sample_radii)):
        # Periodically include logging message
        if count % 10 == 0:
            logging.debug('%s: count: %i' % (options['ap_name'], count))
        count += 1

        np.random.shuffle(I)
        for i in I:
            perturbations = []
            perturbations.append({'ellip': copy(ellip), 'pa': copy(pa)})
            perturbations[-1]['loss'] = _FFT_mean_loss(
                dat,
                sample_radii,
                perturbations[-1]['ellip'],
                perturbations[-1]['pa'],
                i,
                use_center,
                results['background noise'],
                mask=mask,
                reg_scale=regularize_scale if count > 4 else 0,
                name=options['ap_name'])
            for n in range(N_perturb):
                perturbations.append({'ellip': copy(ellip), 'pa': copy(pa)})
                if count % 3 in [0, 1]:
                    perturbations[-1]['ellip'][i] = _x_to_eps(
                        _inv_x_to_eps(perturbations[-1]['ellip'][i]) +
                        np.random.normal(loc=0, scale=perturb_scale[0]))
                if count % 3 in [1, 2]:
                    perturbations[-1]['pa'][i] = (
                        perturbations[-1]['pa'][i] + np.random.normal(
                            loc=0, scale=perturb_scale[1])) % np.pi
                perturbations[-1]['loss'] = _FFT_mean_loss(
                    dat,
                    sample_radii,
                    perturbations[-1]['ellip'],
                    perturbations[-1]['pa'],
                    i,
                    use_center,
                    results['background noise'],
                    mask=mask,
                    reg_scale=regularize_scale if count > 4 else 0,
                    name=options['ap_name'])

            best = np.argmin(list(p['loss'] for p in perturbations))
            if best > 0:
                ellip = copy(perturbations[best]['ellip'])
                pa = copy(perturbations[best]['pa'])
                count_nochange = 0
            else:
                count_nochange += 1

    logging.info('%s: Completed isohpote fit in %i itterations' %
                 (options['ap_name'], count))
    # detect collapsed center
    ######################################################################
    for i in range(5):
        if (_inv_x_to_eps(ellip[i]) - _inv_x_to_eps(ellip[i + 1])) > 0.5:
            ellip[:i + 1] = ellip[i + 1]
            pa[:i + 1] = pa[i + 1]

    # Smooth ellip and pa profile
    ######################################################################
    smooth_ellip = copy(ellip)
    smooth_pa = copy(pa)
    ellip[:3] = min(ellip[:3])
    smooth_ellip = _ellip_smooth(sample_radii, smooth_ellip, 5)
    smooth_pa = _pa_smooth(sample_radii, smooth_pa, 5)

    if 'ap_doplot' in options and options['ap_doplot']:
        ranges = [[
            max(0, int(use_center['x'] - sample_radii[-1] * 1.2)),
            min(dat.shape[1], int(use_center['x'] + sample_radii[-1] * 1.2))
        ],
                  [
                      max(0, int(use_center['y'] - sample_radii[-1] * 1.2)),
                      min(dat.shape[0],
                          int(use_center['y'] + sample_radii[-1] * 1.2))
                  ]]
        LSBImage(dat[ranges[1][0]:ranges[1][1], ranges[0][0]:ranges[0][1]],
                 results['background noise'])
        # plt.imshow(np.clip(dat[ranges[1][0]: ranges[1][1], ranges[0][0]: ranges[0][1]],
        #                    a_min = 0,a_max = None), origin = 'lower', cmap = 'Greys', norm = ImageNormalize(stretch=LogStretch()))
        for i in range(len(sample_radii)):
            plt.gca().add_patch(
                Ellipse((use_center['x'] - ranges[0][0],
                         use_center['y'] - ranges[1][0]),
                        2 * sample_radii[i],
                        2 * sample_radii[i] * (1. - ellip[i]),
                        pa[i] * 180 / np.pi,
                        fill=False,
                        linewidth=((i + 1) / len(sample_radii))**2,
                        color='r'))
        if not ('ap_nologo' in options and options['ap_nologo']):
            AddLogo(plt.gcf())
        plt.savefig(
            '%sfit_ellipse_%s.jpg' %
            (options['ap_plotpath'] if 'ap_plotpath' in options else '',
             options['ap_name']),
            dpi=options['ap_plotdpi'] if 'ap_plotdpi' in options else 300)
        plt.close()

        plt.scatter(sample_radii, ellip, color='r', label='ellip')
        plt.scatter(sample_radii, pa / np.pi, color='b', label='pa/$np.pi$')
        show_ellip = _ellip_smooth(sample_radii, ellip, deg=5)
        show_pa = _pa_smooth(sample_radii, pa, deg=5)
        plt.plot(sample_radii,
                 show_ellip,
                 color='orange',
                 linewidth=2,
                 linestyle='--',
                 label='smooth ellip')
        plt.plot(sample_radii,
                 show_pa / np.pi,
                 color='purple',
                 linewidth=2,
                 linestyle='--',
                 label='smooth pa/$np.pi$')
        #plt.xscale('log')
        plt.legend()
        if not ('ap_nologo' in options and options['ap_nologo']):
            AddLogo(plt.gcf())
        plt.savefig(
            '%sphaseprofile_%s.jpg' %
            (options['ap_plotpath'] if 'ap_plotpath' in options else '',
             options['ap_name']),
            dpi=options['ap_plotdpi'] if 'ap_plotdpi' in options else 300)
        plt.close()

    # Compute errors
    ######################################################################
    ellip_err = np.zeros(len(ellip))
    ellip_err[:2] = np.sqrt(np.sum((ellip[:4] - smooth_ellip[:4])**2) / 4)
    ellip_err[-1] = np.sqrt(np.sum((ellip[-4:] - smooth_ellip[-4:])**2) / 4)
    pa_err = np.zeros(len(pa))
    pa_err[:2] = np.sqrt(np.sum((pa[:4] - smooth_pa[:4])**2) / 4)
    pa_err[-1] = np.sqrt(np.sum((pa[-4:] - smooth_pa[-4:])**2) / 4)
    for i in range(2, len(pa) - 1):
        ellip_err[i] = np.sqrt(
            np.sum((ellip[i - 2:i + 2] - smooth_ellip[i - 2:i + 2])**2) / 4)
        pa_err[i] = np.sqrt(
            np.sum((pa[i - 2:i + 2] - smooth_pa[i - 2:i + 2])**2) / 4)

    res = {
        'fit ellip':
        ellip,
        'fit pa':
        pa,
        'fit R':
        sample_radii,
        'fit ellip_err':
        ellip_err,
        'fit pa_err':
        pa_err,
        'auxfile fitlimit':
        'fit limit semi-major axis: %.2f pix' % sample_radii[-1]
    }
    return IMG, res
Пример #5
0
def Isophote_Initialize_mean(IMG, results, options):
    """Fit global elliptical isophote to a galaxy image using FFT coefficients.

    Same as the default isophote initialization routine, except uses
    mean/std measures for low S/N applications.

    Parameters
    -----------------
    ap_fit_limit : float, default 2
      noise level out to which to extend the fit in units of pixel
      background noise level. Default is 2, smaller values will end
      fitting further out in the galaxy image.

    Notes
    ----------
    :References:
    - 'background'
    - 'background noise'
    - 'psf fwhm'
    - 'center'

    Returns
    -------
    IMG : ndarray
      Unaltered galaxy image

    results : dict
      .. code-block:: python

        {'init ellip': , # Ellipticity of the global fit (float)
         'init pa': ,# Position angle of the global fit (float)
         'init R': ,# Semi-major axis length of global fit (float)
         'auxfile initialize': # optional, message for aux file to record the global ellipticity and postition angle (string)

        }

    """

    ######################################################################
    # Initial attempt to find size of galaxy in image
    # based on when isophotes SB values start to get
    # close to the background noise level
    circ_ellipse_radii = [results["psf fwhm"]]
    allphase = []
    dat = IMG - results["background"]

    while circ_ellipse_radii[-1] < (len(IMG) / 2):
        circ_ellipse_radii.append(circ_ellipse_radii[-1] * (1 + 0.2))
        isovals = _iso_extract(
            dat,
            circ_ellipse_radii[-1],
            {
                "ellip": 0.0,
                "pa": 0.0
            },
            results["center"],
            more=True,
        )
        coefs = fft(isovals[0])
        allphase.append(coefs[2])
        # Stop when at 3 times background noise
        if (np.mean(isovals[0]) < (3 * results["background noise"])
                and len(circ_ellipse_radii) > 4):
            break
    logging.info("%s: init scale: %f pix" %
                 (options["ap_name"], circ_ellipse_radii[-1]))
    # Find global position angle.
    phase = (-Angle_Median(np.angle(allphase[-5:])) /
             2) % np.pi  # (-np.angle(np.mean(allphase[-5:]))/2) % np.pi

    # Find global ellipticity
    test_ellip = np.linspace(0.05, 0.95, 15)
    test_f2 = []
    for e in test_ellip:
        test_f2.append(
            sum(
                list(
                    _fitEllip_mean_loss(
                        e,
                        dat,
                        circ_ellipse_radii[-2] * m,
                        phase,
                        results["center"],
                        results["background noise"],
                    ) for m in np.linspace(0.8, 1.2, 5))))
    ellip = test_ellip[np.argmin(test_f2)]
    res = minimize(
        lambda e, d, r, p, c, n: sum(
            list(
                _fitEllip_mean_loss(_x_to_eps(e[0]), d, r * m, p, c, n)
                for m in np.linspace(0.8, 1.2, 5))),
        x0=_inv_x_to_eps(ellip),
        args=(
            dat,
            circ_ellipse_radii[-2],
            phase,
            results["center"],
            results["background noise"],
        ),
        method="Nelder-Mead",
        options={
            "initial_simplex": [
                [_inv_x_to_eps(ellip) - 1 / 15],
                [_inv_x_to_eps(ellip) + 1 / 15],
            ]
        },
    )
    if res.success:
        logging.debug(
            "%s: using optimal ellipticity %.3f over grid ellipticity %.3f" %
            (options["ap_name"], _x_to_eps(res.x[0]), ellip))
        ellip = _x_to_eps(res.x[0])

    # Compute the error on the parameters
    ######################################################################
    RR = np.linspace(
        circ_ellipse_radii[-2] - results["psf fwhm"],
        circ_ellipse_radii[-2] + results["psf fwhm"],
        10,
    )
    errallphase = []
    for rr in RR:
        isovals = _iso_extract(dat,
                               rr, {
                                   "ellip": 0.0,
                                   "pa": 0.0
                               },
                               results["center"],
                               more=True)
        coefs = fft(isovals[0])
        errallphase.append(coefs[2])
    sample_pas = (-np.angle(1j * np.array(errallphase) / np.mean(errallphase))
                  / 2) % np.pi
    pa_err = np.std(sample_pas)
    res_multi = map(
        lambda rrp: minimize(
            lambda e, d, r, p, c, n: _fitEllip_mean_loss(
                _x_to_eps(e[0]), d, r, p, c, n),
            x0=_inv_x_to_eps(ellip),
            args=(dat, rrp[0], rrp[1], results["center"], results[
                "background noise"]),
            method="Nelder-Mead",
            options={
                "initial_simplex": [
                    [_inv_x_to_eps(ellip) - 1 / 15],
                    [_inv_x_to_eps(ellip) + 1 / 15],
                ]
            },
        ),
        zip(RR, sample_pas),
    )
    ellip_err = np.std(list(_x_to_eps(rm.x[0]) for rm in res_multi))

    circ_ellipse_radii = np.array(circ_ellipse_radii)

    if "ap_doplot" in options and options["ap_doplot"]:

        ranges = [
            [
                max(0,
                    int(results["center"]["x"] -
                        circ_ellipse_radii[-1] * 1.5)),
                min(
                    dat.shape[1],
                    int(results["center"]["x"] + circ_ellipse_radii[-1] * 1.5),
                ),
            ],
            [
                max(0,
                    int(results["center"]["y"] -
                        circ_ellipse_radii[-1] * 1.5)),
                min(
                    dat.shape[0],
                    int(results["center"]["y"] + circ_ellipse_radii[-1] * 1.5),
                ),
            ],
        ]

        LSBImage(
            dat[ranges[1][0]:ranges[1][1], ranges[0][0]:ranges[0][1]],
            results["background noise"],
        )
        # plt.imshow(np.clip(dat[ranges[1][0]: ranges[1][1], ranges[0][0]: ranges[0][1]],a_min = 0, a_max = None),
        #            origin = 'lower', cmap = 'Greys_r', norm = ImageNormalize(stretch=LogStretch()))
        plt.gca().add_patch(
            Ellipse(
                (
                    results["center"]["x"] - ranges[0][0],
                    results["center"]["y"] - ranges[1][0],
                ),
                2 * circ_ellipse_radii[-1],
                2 * circ_ellipse_radii[-1] * (1.0 - ellip),
                phase * 180 / np.pi,
                fill=False,
                linewidth=1,
                color="y",
            ))
        plt.plot(
            [results["center"]["x"] - ranges[0][0]],
            [results["center"]["y"] - ranges[1][0]],
            marker="x",
            markersize=3,
            color="r",
        )
        plt.tight_layout()
        if not ("ap_nologo" in options and options["ap_nologo"]):
            AddLogo(plt.gcf())
        plt.savefig(
            "%sinitialize_ellipse_%s.jpg" % (
                options["ap_plotpath"] if "ap_plotpath" in options else "",
                options["ap_name"],
            ),
            dpi=options["ap_plotdpi"] if "ap_plotdpi" in options else 300,
        )
        plt.close()

        fig, ax = plt.subplots(2, 1, figsize=(6, 6))
        ax[0].plot(
            circ_ellipse_radii[:-1],
            ((-np.angle(allphase) / 2) % np.pi) * 180 / np.pi,
            color="k",
        )
        ax[0].axhline(phase * 180 / np.pi, color="r")
        ax[0].axhline((phase + pa_err) * 180 / np.pi,
                      color="r",
                      linestyle="--")
        ax[0].axhline((phase - pa_err) * 180 / np.pi,
                      color="r",
                      linestyle="--")
        # ax[0].axvline(circ_ellipse_radii[-2], color = 'orange', linestyle = '--')
        ax[0].set_xlabel("Radius [pix]")
        ax[0].set_ylabel("FFT$_{1}$ phase [deg]")
        ax[1].plot(test_ellip, test_f2, color="k")
        ax[1].axvline(ellip, color="r")
        ax[1].axvline(ellip + ellip_err, color="r", linestyle="--")
        ax[1].axvline(ellip - ellip_err, color="r", linestyle="--")
        ax[1].set_xlabel("Ellipticity [1 - b/a]")
        ax[1].set_ylabel("Loss [FFT$_{2}$/med(flux)]")
        plt.tight_layout()
        if not ("ap_nologo" in options and options["ap_nologo"]):
            AddLogo(plt.gcf())
        plt.savefig(
            "%sinitialize_ellipse_optimize_%s.jpg" % (
                options["ap_plotpath"] if "ap_plotpath" in options else "",
                options["ap_name"],
            ),
            dpi=options["ap_plotdpi"] if "ap_plotdpi" in options else 300,
        )
        plt.close()

    auxmessage = (
        "global ellipticity: %.3f +- %.3f, pa: %.3f +- %.3f deg, size: %f pix"
        % (
            ellip,
            ellip_err,
            PA_shift_convention(phase) * 180 / np.pi,
            pa_err * 180 / np.pi,
            circ_ellipse_radii[-2],
        ))
    return IMG, {
        "init ellip": ellip,
        "init ellip_err": ellip_err,
        "init pa": phase,
        "init pa_err": pa_err,
        "init R": circ_ellipse_radii[-2],
        "auxfile initialize": auxmessage,
    }
Пример #6
0
def Isophote_Initialize(IMG, results, options):
    """Fit global elliptical isophote to a galaxy image using FFT coefficients.

    A global position angle and ellipticity are fit in a two step
    process.  First, a series of circular isophotes are geometrically
    sampled until they approach the background level of the image.  An
    FFT is taken for the flux values around each isophote and the
    phase of the second coefficient is used to determine a direction.
    The average direction for the outer isophotes is taken as the
    position angle of the galaxy.  Second, with fixed position angle
    the ellipticity is optimized to minimize the amplitude of the
    second FFT coefficient relative to the median flux in an isophote.

    To compute the error on position angle we use the standard
    deviation of the outer values from step one.  For ellipticity the
    error is computed by optimizing the ellipticity for multiple
    isophotes within 1 PSF length of each other.

    Parameters
    -----------------
    ap_fit_limit : float, default 2
      noise level out to which to extend the fit in units of pixel background noise level. Default is 2, smaller values will end fitting further out in the galaxy image.

    ap_isoinit_pa_set : float, default None
      User set initial position angle in degrees, will override the calculation.

    ap_isoinit_ellip_set : float, default None
      User set initial ellipticity (1 - b/a), will override the calculation.

    Notes
    ----------
    :References:
    - 'background'
    - 'background noise'
    - 'psf fwhm'
    - 'center'

    Returns
    -------
    IMG : ndarray
      Unaltered galaxy image

    results : dict
      .. code-block:: python

        {'init ellip': , # Ellipticity of the global fit (float)
         'init pa': ,# Position angle of the global fit (float)
         'init R': ,# Semi-major axis length of global fit (float)
         'auxfile initialize': # optional, message for aux file to record the global ellipticity and postition angle (string)

        }

    """

    ######################################################################
    # Initial attempt to find size of galaxy in image
    # based on when isophotes SB values start to get
    # close to the background noise level
    circ_ellipse_radii = [1.0]
    allphase = []
    dat = IMG - results["background"]
    mask = results["mask"] if "mask" in results else None
    if not np.any(mask):
        mask = None

    while circ_ellipse_radii[-1] < (len(IMG) / 2):
        circ_ellipse_radii.append(circ_ellipse_radii[-1] * (1 + 0.2))
        isovals = _iso_extract(
            dat,
            circ_ellipse_radii[-1],
            {
                "ellip": 0.0,
                "pa": 0.0
            },
            results["center"],
            more=True,
            mask=mask,
            sigmaclip=True,
            sclip_nsigma=3,
            interp_mask=True,
        )
        coefs = fft(isovals[0])
        allphase.append(coefs[2])
        # Stop when at 3 time background noise
        if (np.quantile(isovals[0], 0.8) < (
            (options["ap_fit_limit"] + 1 if "ap_fit_limit" in options else 3) *
                results["background noise"]) and len(circ_ellipse_radii) > 4):
            break
    logging.info("%s: init scale: %f pix" %
                 (options["ap_name"], circ_ellipse_radii[-1]))
    # Find global position angle.
    phase = (-Angle_Median(np.angle(allphase[-5:])) / 2) % np.pi
    if "ap_isoinit_pa_set" in options:
        phase = PA_shift_convention(options["ap_isoinit_pa_set"] * np.pi / 180)

    # Find global ellipticity
    test_ellip = np.linspace(0.05, 0.95, 15)
    test_f2 = []
    for e in test_ellip:
        test_f2.append(
            sum(
                list(
                    _fitEllip_loss(
                        e,
                        dat,
                        circ_ellipse_radii[-2] * m,
                        phase,
                        results["center"],
                        results["background noise"],
                        mask,
                    ) for m in np.linspace(0.8, 1.2, 5))))
    ellip = test_ellip[np.argmin(test_f2)]
    res = minimize(
        lambda e, d, r, p, c, n, msk: sum(
            list(
                _fitEllip_loss(_x_to_eps(e[0]), d, r * m, p, c, n, msk)
                for m in np.linspace(0.8, 1.2, 5))),
        x0=_inv_x_to_eps(ellip),
        args=(
            dat,
            circ_ellipse_radii[-2],
            phase,
            results["center"],
            results["background noise"],
            mask,
        ),
        method="Nelder-Mead",
        options={
            "initial_simplex": [
                [_inv_x_to_eps(ellip) - 1 / 15],
                [_inv_x_to_eps(ellip) + 1 / 15],
            ]
        },
    )
    if res.success:
        logging.debug(
            "%s: using optimal ellipticity %.3f over grid ellipticity %.3f" %
            (options["ap_name"], _x_to_eps(res.x[0]), ellip))
        ellip = _x_to_eps(res.x[0])
    if "ap_isoinit_ellip_set" in options:
        ellip = options["ap_isoinit_ellip_set"]

    # Compute the error on the parameters
    ######################################################################
    RR = np.linspace(
        circ_ellipse_radii[-2] - results["psf fwhm"],
        circ_ellipse_radii[-2] + results["psf fwhm"],
        10,
    )
    errallphase = []
    for rr in RR:
        isovals = _iso_extract(
            dat,
            rr,
            {
                "ellip": 0.0,
                "pa": 0.0
            },
            results["center"],
            more=True,
            sigmaclip=True,
            sclip_nsigma=3,
            interp_mask=True,
        )
        coefs = fft(isovals[0])
        errallphase.append(coefs[2])
    sample_pas = (-np.angle(1j * np.array(errallphase) / np.mean(errallphase))
                  / 2) % np.pi
    pa_err = iqr(sample_pas, rng=[16, 84]) / 2
    res_multi = map(
        lambda rrp: minimize(
            lambda e, d, r, p, c, n, m: _fitEllip_loss(_x_to_eps(e[0]), d, r,
                                                       p, c, n, m),
            x0=_inv_x_to_eps(ellip),
            args=(
                dat,
                rrp[0],
                rrp[1],
                results["center"],
                results["background noise"],
                mask,
            ),
            method="Nelder-Mead",
            options={
                "initial_simplex": [
                    [_inv_x_to_eps(ellip) - 1 / 15],
                    [_inv_x_to_eps(ellip) + 1 / 15],
                ]
            },
        ),
        zip(RR, sample_pas),
    )
    ellip_err = iqr(list(_x_to_eps(rm.x[0])
                         for rm in res_multi), rng=[16, 84]) / 2

    circ_ellipse_radii = np.array(circ_ellipse_radii)

    if "ap_doplot" in options and options["ap_doplot"]:
        Plot_Isophote_Init_Ellipse(dat, circ_ellipse_radii, ellip, phase,
                                   results, options)
        Plot_Isophote_Init_Optimize(
            circ_ellipse_radii,
            allphase,
            phase,
            pa_err,
            test_ellip,
            test_f2,
            ellip,
            ellip_err,
            results,
            options,
        )

    auxmessage = (
        "global ellipticity: %.3f +- %.3f, pa: %.3f +- %.3f deg, size: %f pix"
        % (
            ellip,
            ellip_err,
            PA_shift_convention(phase) * 180 / np.pi,
            pa_err * 180 / np.pi,
            circ_ellipse_radii[-2],
        ))
    return IMG, {
        "init ellip": ellip,
        "init ellip_err": ellip_err,
        "init pa": phase,
        "init pa_err": pa_err,
        "init R": circ_ellipse_radii[-2],
        "auxfile initialize": auxmessage,
    }
Пример #7
0
def Isophote_Extract(IMG, results, options):
    """General method for extracting SB profiles.

    The default SB profile extraction method is highly
    flexible, allowing users to test a variety of techniques on their data
    to determine the most robust. The user may specify a variety of
    sampling arguments for the photometry extraction.  For example, a
    start or end radius in pixels, or whether to sample geometrically or
    linearly in radius.  Geometric sampling is the default as it is
    faster.  Once the sampling profile of semi-major axis values has been
    chosen, the function interpolates (spline) the position angle and
    ellipticity profiles at the requested values.  For any sampling beyond
    the outer radius from the *Isophotal Fitting* step, a constant value
    is used.  Within 1 PSF, a circular isophote is used.

    Parameters
    -----------------
    ap_zeropoint : float, default 22.5
      Photometric zero point. For converting flux to mag units.

    ap_fluxunits : str, default "mag"
      units for outputted photometry. Can either be "mag" for log
      units, or "intensity" for linear units.
    
    ap_samplegeometricscale : float, default 0.1
      growth scale for isophotes when sampling for the final output
      profile.  Used when sampling geometrically. By default, each
      isophote is 10\% further than the last.

    ap_samplelinearscale : float, default None
      growth scale (in pixels) for isophotes when sampling for the
      final output profile. Used when sampling linearly. Default is 1
      PSF length.

    ap_samplestyle : string, default 'geometric'
      indicate if isophote sampling radii should grow linearly or
      geometrically. Can also do geometric sampling at the center and
      linear sampling once geometric step size equals linear. Options
      are: 'linear', 'geometric', 'geometric-linear'

    ap_sampleinitR : float, default None
      Starting radius (in pixels) for isophote sampling from the
      image. Note that a starting radius of zero is not
      advised. Default is 1 pixel or 1PSF, whichever is smaller.

    ap_sampleendR : float, default None
      End radius (in pixels) for isophote sampling from the
      image. Default is 3 times the fit radius, also see
      *ap_extractfull*.

    ap_isoband_start : float, default 2
      The noise level at which to begin sampling a band of pixels to
      compute SB instead of sampling a line of pixels near the
      isophote in units of pixel flux noise. Will never initiate band
      averaging if the band width is less than half a pixel

    ap_isoband_width : float, default 0.025
      The relative size of the isophote bands to sample. flux values
      will be sampled at +- *ap_isoband_width* \*R for each radius.

    ap_isoband_fixed : bool, default False
      Use a fixed width for the size of the isobands, the width is set
      by *ap_isoband_width* which now has units of pixels, the default
      is 0.5 such that the full band has a width of 1 pixel.

    ap_truncate_evaluation : bool, default False
      Stop evaluating new isophotes once two negative flux isophotes
      have been recorded, presumed to have reached the end of the
      profile.

    ap_extractfull : bool, default False
      Tells AutoProf to extend the isophotal solution to the edge of
      the image. Will be overridden by *ap_truncate_evaluation*.

    ap_iso_interpolate_start : float, default 5
      Use a Lanczos interpolation for isophotes with semi-major axis
      less than this number times the PSF.

    ap_iso_interpolate_method : string, default 'lanczos'
      Select method for flux interpolation on image, options are
      'lanczos' and 'bicubic'. Default is 'lanczos' with a window size
      of 3.

    ap_iso_interpolate_window : int, default 3
      Window size for Lanczos interpolation, default is 3, meaning 3
      pixels on either side of the sample point are used for
      interpolation.

    ap_isoaverage_method : string, default 'median'
      Select the method used to compute the averafge flux along an
      isophote. Choose from 'mean', 'median', and 'mode'.  In general,
      median is fast and robust to a few outliers. Mode is slow but
      robust to more outliers. Mean is fast and accurate in low S/N
      regimes where fluxes take on near integer values, but not robust
      to outliers. The mean should be used along with a mask to remove
      spurious objects such as foreground stars or galaxies, and
      should always be used with caution.

    ap_isoclip : bool, default False
      Perform sigma clipping along extracted isophotes. Removes flux
      samples from an isophote that deviate significantly from the
      median. Several iterations of sigma clipping are performed until
      convergence or *ap_isoclip_iterations* iterations are
      reached. Sigma clipping is a useful substitute for masking
      objects, though careful masking is better. Also an aggressive
      sigma clip may bias results.

    ap_isoclip_iterations : int, default None
      Maximum number of sigma clipping iterations to perform. The
      default is infinity, so the sigma clipping procedure repeats
      until convergence

    ap_isoclip_nsigma : float, default 5
      Number of sigma above median to apply clipping. All values above
      (median + *ap_isoclip_nsigma* x sigma) are removed from the
      isophote.

    ap_iso_measurecoefs : tuple, default None
      tuple indicating which fourier modes to extract along fitted
      isophotes. Most common is (4,), which identifies boxy/disky
      isophotes. Also common is (1,3), which identifies lopsided
      galaxies. The outputted values are computed as a_i =
      imag(F_i)/abs(F_0) and b_i = real(F_i)/abs(F_0) where F_i is a
      fourier coefficient. Not activated by default as it adds to
      computation time.

    ap_plot_sbprof_ylim : tuple, default None
      Tuple with axes limits for the y-axis in the SB profile
      diagnostic plot. Be careful when using intensity units
      since this will change the ideal axis limits.
    
    ap_plot_sbprof_xlim : tuple, default None
      Tuple with axes limits for the x-axis in the SB profile
      diagnostic plot.
    
    ap_plot_sbprof_set_errscale : float, default None
      Float value by which to scale errorbars on the SB profile
      this makes them more visible in cases where the statistical
      errors are very small.
    
    Notes
    ----------
    :References:
    - 'background'
    - 'background noise'
    - 'psf fwhm'
    - 'center'
    - 'init ellip'
    - 'init pa'
    - 'fit R'
    - 'fit ellip'
    - 'fit pa'
    - 'fit ellip_err' (optional)
    - 'fit pa_err' (optional)

    Returns
    -------
    IMG : ndarray
      Unaltered galaxy image

    results : dict
      .. code-block:: python

        {'prof header': , # List object with strings giving the items in the header of the final SB profile (list)
         'prof units': , # dict object that links header strings to units (given as strings) for each variable (dict)
         'prof data': # dict object linking header strings to list objects containing the rows for a given variable (dict)

        }

    """
    use_center = results["center"]

    # Radius values to evaluate isophotes
    R = [
        options["ap_sampleinitR"]
        if "ap_sampleinitR" in options
        else min(1.0, results["psf fwhm"] / 2)
    ]
    while (
        (
            (R[-1] < options["ap_sampleendR"] if "ap_sampleendR" in options else True)
            and R[-1] < 3 * results["fit R"][-1]
        )
        or (options["ap_extractfull"] if "ap_extractfull" in options else False)
    ) and R[-1] < max(IMG.shape) / np.sqrt(2):
        if (
            "ap_samplestyle" in options
            and options["ap_samplestyle"] == "geometric-linear"
        ):
            if len(R) > 1 and abs(R[-1] - R[-2]) >= (
                options["ap_samplelinearscale"]
                if "ap_samplelinearscale" in options
                else 3 * results["psf fwhm"]
            ):
                R.append(
                    R[-1]
                    + (
                        options["ap_samplelinearscale"]
                        if "ap_samplelinearscale" in options
                        else results["psf fwhm"] / 2
                    )
                )
            else:
                R.append(
                    R[-1]
                    * (
                        1.0
                        + (
                            options["ap_samplegeometricscale"]
                            if "ap_samplegeometricscale" in options
                            else 0.1
                        )
                    )
                )
        elif "ap_samplestyle" in options and options["ap_samplestyle"] == "linear":
            R.append(
                R[-1]
                + (
                    options["ap_samplelinearscale"]
                    if "ap_samplelinearscale" in options
                    else 0.5 * results["psf fwhm"]
                )
            )
        else:
            R.append(
                R[-1]
                * (
                    1.0
                    + (
                        options["ap_samplegeometricscale"]
                        if "ap_samplegeometricscale" in options
                        else 0.1
                    )
                )
            )
    R = np.array(R)
    logging.info(
        "%s: R complete in range [%.1f,%.1f]" % (options["ap_name"], R[0], R[-1])
    )

    # Interpolate profile values, when extrapolating just take last point
    tmp_pa_s = UnivariateSpline(
        results["fit R"], np.sin(2 * results["fit pa"]), ext=3, s=0
    )(R)
    tmp_pa_c = UnivariateSpline(
        results["fit R"], np.cos(2 * results["fit pa"]), ext=3, s=0
    )(R)
    E = _x_to_eps(
        UnivariateSpline(
            results["fit R"], _inv_x_to_eps(results["fit ellip"]), ext=3, s=0
        )(R)
    )
    # np.arctan(tmp_pa_s / tmp_pa_c) + (np.pi * (tmp_pa_c < 0))
    PA = _x_to_pa(((np.arctan2(tmp_pa_s, tmp_pa_c)) % (2 * np.pi)) / 2) 
    parameters = list({"ellip": E[i], "pa": PA[i]} for i in range(len(R)))

    if "fit Fmodes" in results:
        for i in range(len(R)):
            parameters[i]["m"] = results["fit Fmodes"]
            parameters[i]["Am"] = np.array(
                list(
                    UnivariateSpline(
                        results["fit R"],
                        results["fit Fmode A%i" % results["fit Fmodes"][m]],
                        ext=3,
                        s=0,
                    )(R[i])
                    for m in range(len(results["fit Fmodes"]))
                )
            )
            parameters[i]["Phim"] = np.array(
                list(
                    UnivariateSpline(
                        results["fit R"],
                        results["fit Fmode Phi%i" % results["fit Fmodes"][m]],
                        ext=3,
                        s=0,
                    )(R[i])
                    for m in range(len(results["fit Fmodes"]))
                )
            )

    if "fit C" in results:
        CC = UnivariateSpline(results["fit R"], results["fit C"], ext=3, s=0)(R)
        for i in range(len(R)):
            parameters[i]["C"] = CC[i]

    # Get errors for pa and ellip
    for i in range(len(R)):
        if (
            "fit ellip_err" in results
            and (not results["fit ellip_err"] is None)
            and "fit pa_err" in results
            and (not results["fit pa_err"] is None)
        ):
            parameters[i]["ellip err"] = np.clip(
                UnivariateSpline(
                    results["fit R"], results["fit ellip_err"], ext=3, s=0
                )(R[i]),
                a_min=1e-3,
                a_max=None,
            )
            parameters[i]["pa err"] = np.clip(
                UnivariateSpline(results["fit R"], results["fit pa_err"], ext=3, s=0)(
                    R[i]
                ),
                a_min=1e-3,
                a_max=None,
            )
        else:
            parameters[i]["ellip err"] = 0.0
            parameters[i]["pa err"] = 0.0

    return IMG, _Generate_Profile(IMG, results, R, parameters, options)
Пример #8
0
def Isophote_Fit_FFT_mean(IMG, results, options):
    """Fit elliptical isophotes to a galaxy image using FFT coefficients and regularization.

    Same as the standard isophote fitting routine, except uses less
    robust mean/std measures. This is only intended for low S/N data
    where pixels have low integer counts.

    Parameters
    -----------------
    ap_scale : float, default 0.2
      growth scale when fitting isophotes, not the same as
      *ap_sample---scale*.

    ap_fit_limit : float, default 2
      noise level out to which to extend the fit in units of pixel
      background noise level. Default is 2, smaller values will end
      fitting further out in the galaxy image.

    ap_regularize_scale : float, default 1
      scale factor to apply to regularization coupling factor between
      isophotes.  Default of 1, larger values make smoother fits,
      smaller values give more chaotic fits.

    Notes
    ----------
    :References:
    - 'background'
    - 'background noise'
    - 'center'
    - 'psf fwhm'
    - 'init ellip'
    - 'init pa'

    Returns
    -------
    IMG : ndarray
      Unaltered galaxy image

    results : dict
      .. code-block:: python

        {'fit ellip': , # array of ellipticity values (ndarray)
         'fit pa': , # array of PA values (ndarray)
         'fit R': , # array of semi-major axis values (ndarray)
         'fit ellip_err': , # optional, array of ellipticity error values (ndarray)
         'fit pa_err': , # optional, array of PA error values (ndarray)
         'auxfile fitlimit': # optional, auxfile message (string)

        }

    """

    if "ap_scale" in options:
        scale = options["ap_scale"]
    else:
        scale = 0.2

    # subtract background from image during processing
    dat = IMG - results["background"]
    mask = results["mask"] if "mask" in results else None
    if not np.any(mask):
        mask = None

    # Determine sampling radii
    ######################################################################
    shrink = 0
    while shrink < 5:
        sample_radii = [3 * results["psf fwhm"] / 2]
        while sample_radii[-1] < (max(IMG.shape) / 2):
            isovals = _iso_extract(
                dat,
                sample_radii[-1],
                {
                    "ellip": results["init ellip"],
                    "pa": results["init pa"]
                },
                results["center"],
                more=False,
                mask=mask,
            )
            if (np.mean(isovals) <
                (options["ap_fit_limit"] if "ap_fit_limit" in options else 1) *
                    results["background noise"]):
                break
            sample_radii.append(sample_radii[-1] * (1.0 + scale /
                                                    (1.0 + shrink)))
        if len(sample_radii) < 15:
            shrink += 1
        else:
            break
    if shrink >= 5:
        raise Exception(
            "Unable to initialize ellipse fit, check diagnostic plots. Possible missed center."
        )
    ellip = np.ones(len(sample_radii)) * results["init ellip"]
    pa = np.ones(len(sample_radii)) * results["init pa"]
    logging.debug("%s: sample radii: %s" %
                  (options["ap_name"], str(sample_radii)))

    # Fit isophotes
    ######################################################################
    perturb_scale = np.array([0.03, 0.06])
    regularize_scale = (options["ap_regularize_scale"]
                        if "ap_regularize_scale" in options else 1.0)
    N_perturb = 5

    count = 0

    count_nochange = 0
    use_center = copy(results["center"])
    I = np.array(range(len(sample_radii)))
    while count < 300 and count_nochange < (3 * len(sample_radii)):
        # Periodically include logging message
        if count % 10 == 0:
            logging.debug("%s: count: %i" % (options["ap_name"], count))
        count += 1

        np.random.shuffle(I)
        for i in I:
            perturbations = []
            perturbations.append({"ellip": copy(ellip), "pa": copy(pa)})
            perturbations[-1]["loss"] = _FFT_mean_loss(
                dat,
                sample_radii,
                perturbations[-1]["ellip"],
                perturbations[-1]["pa"],
                i,
                use_center,
                results["background noise"],
                mask=mask,
                reg_scale=regularize_scale if count > 4 else 0,
                name=options["ap_name"],
            )
            for n in range(N_perturb):
                perturbations.append({"ellip": copy(ellip), "pa": copy(pa)})
                if count % 3 in [0, 1]:
                    perturbations[-1]["ellip"][i] = _x_to_eps(
                        _inv_x_to_eps(perturbations[-1]["ellip"][i]) +
                        np.random.normal(loc=0, scale=perturb_scale[0]))
                if count % 3 in [1, 2]:
                    perturbations[-1]["pa"][i] = (
                        perturbations[-1]["pa"][i] + np.random.normal(
                            loc=0, scale=perturb_scale[1])) % np.pi
                perturbations[-1]["loss"] = _FFT_mean_loss(
                    dat,
                    sample_radii,
                    perturbations[-1]["ellip"],
                    perturbations[-1]["pa"],
                    i,
                    use_center,
                    results["background noise"],
                    mask=mask,
                    reg_scale=regularize_scale if count > 4 else 0,
                    name=options["ap_name"],
                )

            best = np.argmin(list(p["loss"] for p in perturbations))
            if best > 0:
                ellip = copy(perturbations[best]["ellip"])
                pa = copy(perturbations[best]["pa"])
                count_nochange = 0
            else:
                count_nochange += 1

    logging.info("%s: Completed isohpote fit in %i itterations" %
                 (options["ap_name"], count))
    # detect collapsed center
    ######################################################################
    for i in range(5):
        if (_inv_x_to_eps(ellip[i]) - _inv_x_to_eps(ellip[i + 1])) > 0.5:
            ellip[:i + 1] = ellip[i + 1]
            pa[:i + 1] = pa[i + 1]

    # Smooth ellip and pa profile
    ######################################################################
    smooth_ellip = copy(ellip)
    smooth_pa = copy(pa)
    ellip[:3] = min(ellip[:3])
    smooth_ellip = _ellip_smooth(sample_radii, smooth_ellip, 5)
    smooth_pa = _pa_smooth(sample_radii, smooth_pa, 5)

    if "ap_doplot" in options and options["ap_doplot"]:
        ranges = [
            [
                max(0, int(use_center["x"] - sample_radii[-1] * 1.2)),
                min(dat.shape[1],
                    int(use_center["x"] + sample_radii[-1] * 1.2)),
            ],
            [
                max(0, int(use_center["y"] - sample_radii[-1] * 1.2)),
                min(dat.shape[0],
                    int(use_center["y"] + sample_radii[-1] * 1.2)),
            ],
        ]
        LSBImage(
            dat[ranges[1][0]:ranges[1][1], ranges[0][0]:ranges[0][1]],
            results["background noise"],
        )
        # plt.imshow(np.clip(dat[ranges[1][0]: ranges[1][1], ranges[0][0]: ranges[0][1]],
        #                    a_min = 0,a_max = None), origin = 'lower', cmap = 'Greys', norm = ImageNormalize(stretch=LogStretch()))
        for i in range(len(sample_radii)):
            plt.gca().add_patch(
                Ellipse(
                    (use_center["x"] - ranges[0][0],
                     use_center["y"] - ranges[1][0]),
                    2 * sample_radii[i],
                    2 * sample_radii[i] * (1.0 - ellip[i]),
                    pa[i] * 180 / np.pi,
                    fill=False,
                    linewidth=((i + 1) / len(sample_radii))**2,
                    color="r",
                ))
        if not ("ap_nologo" in options and options["ap_nologo"]):
            AddLogo(plt.gcf())
        plt.savefig(
            "%sfit_ellipse_%s.jpg" % (
                options["ap_plotpath"] if "ap_plotpath" in options else "",
                options["ap_name"],
            ),
            dpi=options["ap_plotdpi"] if "ap_plotdpi" in options else 300,
        )
        plt.close()

        plt.scatter(sample_radii, ellip, color="r", label="ellip")
        plt.scatter(sample_radii, pa / np.pi, color="b", label="pa/$np.pi$")
        show_ellip = _ellip_smooth(sample_radii, ellip, deg=5)
        show_pa = _pa_smooth(sample_radii, pa, deg=5)
        plt.plot(
            sample_radii,
            show_ellip,
            color="orange",
            linewidth=2,
            linestyle="--",
            label="smooth ellip",
        )
        plt.plot(
            sample_radii,
            show_pa / np.pi,
            color="purple",
            linewidth=2,
            linestyle="--",
            label="smooth pa/$np.pi$",
        )
        # plt.xscale('log')
        plt.legend()
        if not ("ap_nologo" in options and options["ap_nologo"]):
            AddLogo(plt.gcf())
        plt.savefig(
            "%sphaseprofile_%s.jpg" % (
                options["ap_plotpath"] if "ap_plotpath" in options else "",
                options["ap_name"],
            ),
            dpi=options["ap_plotdpi"] if "ap_plotdpi" in options else 300,
        )
        plt.close()

    # Compute errors
    ######################################################################
    ellip_err = np.zeros(len(ellip))
    ellip_err[:2] = np.sqrt(np.sum((ellip[:4] - smooth_ellip[:4])**2) / 4)
    ellip_err[-1] = np.sqrt(np.sum((ellip[-4:] - smooth_ellip[-4:])**2) / 4)
    pa_err = np.zeros(len(pa))
    pa_err[:2] = np.sqrt(np.sum((pa[:4] - smooth_pa[:4])**2) / 4)
    pa_err[-1] = np.sqrt(np.sum((pa[-4:] - smooth_pa[-4:])**2) / 4)
    for i in range(2, len(pa) - 1):
        ellip_err[i] = np.sqrt(
            np.sum((ellip[i - 2:i + 2] - smooth_ellip[i - 2:i + 2])**2) / 4)
        pa_err[i] = np.sqrt(
            np.sum((pa[i - 2:i + 2] - smooth_pa[i - 2:i + 2])**2) / 4)

    res = {
        "fit ellip":
        ellip,
        "fit pa":
        pa,
        "fit R":
        sample_radii,
        "fit ellip_err":
        ellip_err,
        "fit pa_err":
        pa_err,
        "auxfile fitlimit":
        "fit limit semi-major axis: %.2f pix" % sample_radii[-1],
    }
    return IMG, res
Пример #9
0
def Isophote_Fit_FFT_Robust(IMG, results, options):
    """Fit elliptical isophotes to a galaxy image using FFT coefficients and regularization.

    The isophotal fitting routine simultaneously optimizes a
    collection of elliptical isophotes by minimizing the 2nd FFT
    coefficient power, regularized for robustness. A series of
    isophotes are constructed which grow geometrically until they
    begin to reach the background level.  Then the algorithm
    iteratively updates the position angle and ellipticity of each
    isophote individually for many rounds.  Each round updates every
    isophote in a random order.  Each round cycles between three
    options: optimizing position angle, ellipticity, or both.  To
    optimize the parameters, 5 values (pa, ellip, or both) are
    randomly sampled and the "loss" is computed.  The loss is a
    combination of the relative amplitude of the second FFT
    coefficient (compared to the median flux), and a regularization
    term.  The regularization term penalizes adjacent isophotes for
    having different position angle or ellipticity (using the l1
    norm).  Thus, all the isophotes are coupled and tend to fit
    smoothly varying isophotes.  When the optimization has completed
    three rounds without any isophotes updating, the profile is
    assumed to have converged.

    An uncertainty for each ellipticity and position angle value is
    determined by repeatedly re-optimizing each ellipse with slight
    adjustments to it's semi-major axis length (+- 5%). The standard
    deviation of the PA/ellipticity after repeated fitting gives the
    uncertainty.

    Parameters
    -----------------
    ap_scale : float, default 0.2
      growth scale when fitting isophotes, not the same as
      *ap_sample---scale*.

    ap_fit_limit : float, default 2
      noise level out to which to extend the fit in units of pixel
      background noise level. Default is 2, smaller values will end
      fitting further out in the galaxy image.

    ap_regularize_scale : float, default 1
      scale factor to apply to regularization coupling factor between
      isophotes.  Default of 1, larger values make smoother fits,
      smaller values give more chaotic fits.

    ap_isofit_robustclip : float, default 0.15
      quantile of flux values at which to clip when extracting values
      along an isophote. Clipping outlier values (such as very bright
      stars) while fitting isophotes allows for robust computation of
      FFT coefficients along an isophote.

    ap_isofit_losscoefs : tuple, default (2,)
      Tuple of FFT coefficients to use in optimization
      procedure. AutoProf will attemp to minimize the power in all
      listed FFT coefficients. Must be a tuple, not a list.

    ap_isofit_superellipse : bool, default False
      If True, AutoProf will fit superellipses instead of regular
      ellipses. A superellipse is typically used to represent
      boxy/disky isophotes. The variable controlling the transition
      from a rectangle to an ellipse to a four-armed-star like shape
      is C. A value of C = 2 represents an ellipse and is the starting
      point of the optimization.

    ap_isofit_fitcoefs : tuple, default None
      Tuple of FFT coefficients to use in fitting procedure. AutoProf
      will attemp to fit ellipses with these Fourier mode
      perturbations. Such perturbations allow for lopsided, boxy,
      disky, and other types of isophotes beyond straightforward
      ellipses. Must be a tuple, not a list. Note that AutoProf will
      first fit ellipses, then turn on the Fourier mode perturbations,
      thus the fitting time will always be longer.

    ap_isofit_fitcoefs_FFTinit : bool, default False
      If True, the coefficients for the Fourier modes fitted from
      ap_isofit_fitcoefs will be initialized using an FFT
      decomposition along fitted elliptical isophotes. This can
      improve the fit result, though it is less stable and so users
      should examine the results after fitting.

    ap_isofit_perturbscale_ellip : float, default 0.03
      Sampling scale for random adjustments to ellipticity made while
      optimizing isophotes. Smaller values will converge faster, but
      get stuck in local minima; larger values will escape local
      minima, but takes longer to converge.

    ap_isofit_perturbscale_pa : float, default 0.06
      Sampling scale for random adjustments to position angle made
      while optimizing isophotes. Smaller values will converge faster,
      but get stuck in local minima; larger values will escape local
      minima, but takes longer to converge.

    ap_isofit_iterlimitmax : int, default 300
      Maximum number of iterations (each iteration adjusts every
      isophote once) before automatically stopping optimization. For
      galaxies with lots of structure (ie detailed spiral arms) more
      iterations may be needed to fully fit the light distribution,
      but runtime will be longer.

    ap_isofit_iterlimitmin : int, default 0
      Minimum number of iterations before optimization is allowed to
      stop.

    ap_isofit_iterstopnochange : float, default 3
      Number of iterations with no updates to parameters before
      optimization procedure stops. Lower values will process galaxies
      faster, but may still be stuck in local minima, higher values
      are more likely to converge on the global minimum but can take a
      long time to run. Fractional values are allowed though not
      recomended.

    Notes
    ----------
    :References:
    - 'background'
    - 'background noise'
    - 'psf fwhm'
    - 'center'
    - 'mask' (optional)
    - 'init ellip'
    - 'init pa'

    Returns
    -------
    IMG : ndarray
      Unaltered galaxy image

    results : dict
      .. code-block:: python

        {'fit ellip': , # array of ellipticity values (ndarray)
         'fit pa': , # array of PA values (ndarray)
         'fit R': , # array of semi-major axis values (ndarray)
         'fit ellip_err': , # optional, array of ellipticity error values (ndarray)
         'fit pa_err': , # optional, array of PA error values (ndarray)
         'fit C': , # optional, superellipse scale parameter (ndarray)
         'fit Fmodes': , # optional, fitted Fourier mode indices (tuple)
         'fit Fmode A*': , # optional, fitted Fourier mode amplitudes, * for each index (ndarray)
         'fit Fmode Phi*': , # optional, fitted Fourier mode phases, * for each index (ndarray)
         'auxfile fitlimit': # optional, auxfile message (string)

        }

    """

    if "ap_scale" in options:
        scale = options["ap_scale"]
    else:
        scale = 0.2

    # subtract background from image during processing
    dat = IMG - results["background"]
    mask = results["mask"] if "mask" in results else None
    if not np.any(mask):
        mask = None

    # Determine sampling radii
    ######################################################################
    shrink = 0
    while shrink < 5:
        sample_radii = [max(1.0, results["psf fwhm"] / 2)]
        while sample_radii[-1] < (max(IMG.shape) / 2):
            isovals = _iso_extract(
                dat,
                sample_radii[-1],
                {
                    "ellip": results["init ellip"],
                    "pa": results["init pa"]
                },
                results["center"],
                more=False,
                mask=mask,
            )
            if (np.median(isovals) <
                (options["ap_fit_limit"] if "ap_fit_limit" in options else 2) *
                    results["background noise"]):
                break
            sample_radii.append(sample_radii[-1] * (1.0 + scale /
                                                    (1.0 + shrink)))
        if len(sample_radii) < 15:
            shrink += 1
        else:
            break
    if shrink >= 5:
        raise Exception(
            "Unable to initialize ellipse fit, check diagnostic plots. Possible missed center."
        )
    ellip = np.ones(len(sample_radii)) * results["init ellip"]
    pa = np.ones(len(sample_radii)) * results["init pa"]
    logging.debug("%s: sample radii: %s" %
                  (options["ap_name"], str(sample_radii)))
    # Fit isophotes
    ######################################################################
    perturb_scale = 0.03
    regularize_scale = (options["ap_regularize_scale"]
                        if "ap_regularize_scale" in options else 1.0)
    robust_clip = (options["ap_isofit_robustclip"]
                   if "ap_isofit_robustclip" in options else 0.15)
    N_perturb = 5
    fit_coefs = (options["ap_isofit_losscoefs"]
                 if "ap_isofit_losscoefs" in options else None)
    fit_params = (options["ap_isofit_fitcoefs"]
                  if "ap_isofit_fitcoefs" in options else None)
    fit_superellipse = (options["ap_isofit_superellipse"]
                        if "ap_isofit_superellipse" in options else False)
    parameters = list(
        {
            "ellip": ellip[i],
            "pa": pa[i],
            "m": fit_params,
            "C": 2 if fit_superellipse else None,
            "Am": None if fit_params is None else np.zeros(len(fit_params)),
            "Phim": None if fit_params is None else np.zeros(len(fit_params)),
        } for i in range(len(ellip)))

    count = 0

    iterlimitmax = (options["ap_isofit_iterlimitmax"]
                    if "ap_isofit_iterlimitmax" in options else 1000)
    iterlimitmin = (options["ap_isofit_iterlimitmin"]
                    if "ap_isofit_iterlimitmin" in options else 0)
    iterstopnochange = (options["ap_isofit_iterstopnochange"]
                        if "ap_isofit_iterstopnochange" in options else 3)
    count_nochange = 0
    use_center = copy(results["center"])
    I = np.array(range(len(sample_radii)))
    param_cycle = 2
    base_params = 2 + int(fit_superellipse)
    while count < iterlimitmax:
        # Periodically include logging message
        if count % 10 == 0:
            logging.debug("%s: count: %i" % (options["ap_name"], count))
        count += 1

        np.random.shuffle(I)
        N_perturb = int(1 + (10 / np.sqrt(count)))

        for i in I:
            perturbations = []
            perturbations.append(deepcopy(parameters))
            perturbations[-1][i]["loss"] = _FFT_Robust_loss(
                dat,
                sample_radii,
                perturbations[-1],
                i,
                use_center,
                results["background noise"],
                mask=mask,
                reg_scale=regularize_scale if count > 4 else 0,
                robust_clip=robust_clip,
                fit_coefs=fit_coefs,
                name=options["ap_name"],
            )
            for n in range(N_perturb):
                perturbations.append(deepcopy(parameters))
                if count % param_cycle == 0:
                    perturbations[-1][i]["ellip"] = _x_to_eps(
                        _inv_x_to_eps(perturbations[-1][i]["ellip"]) +
                        np.random.normal(loc=0, scale=perturb_scale))
                elif count % param_cycle == 1:
                    perturbations[-1][i]["pa"] = (
                        perturbations[-1][i]["pa"] + np.random.normal(
                            loc=0, scale=np.pi * perturb_scale)) % np.pi
                elif (count %
                      param_cycle) == 2 and not parameters[i]["C"] is None:
                    perturbations[-1][i]["C"] = 10**(
                        np.log10(perturbations[-1][i]["C"]) + np.random.normal(
                            loc=0, scale=np.log10(1.0 + perturb_scale)))
                elif count % param_cycle < (base_params +
                                            len(parameters[i]["m"])):
                    perturbations[-1][i]["Am"][
                        (count % param_cycle) -
                        base_params] += np.random.normal(loc=0,
                                                         scale=perturb_scale)
                elif count % param_cycle < (base_params +
                                            2 * len(parameters[i]["m"])):
                    phim_index = ((count % param_cycle) - base_params -
                                  len(parameters[i]["m"]))
                    perturbations[-1][i]["Phim"][phim_index] = (
                        perturbations[-1][i]["Phim"][phim_index] +
                        np.random.normal(
                            loc=0,
                            scale=2 * np.pi * perturb_scale /
                            parameters[i]["m"][phim_index],
                        )) % (2 * np.pi / parameters[i]["m"][phim_index])
                else:
                    raise Exception(
                        "Unrecognized optimization parameter id: %i" %
                        (count % param_cycle))
                perturbations[-1][i]["loss"] = _FFT_Robust_loss(
                    dat,
                    sample_radii,
                    perturbations[-1],
                    i,
                    use_center,
                    results["background noise"],
                    mask=mask,
                    reg_scale=regularize_scale if count > 4 else 0,
                    robust_clip=robust_clip,
                    fit_coefs=fit_coefs,
                    name=options["ap_name"],
                )

            best = np.argmin(list(p[i]["loss"] for p in perturbations))
            if best > 0:
                parameters = deepcopy(perturbations[best])
                del parameters[i]["loss"]
                count_nochange = 0
            else:
                count_nochange += 1
            if not (count_nochange <
                    (iterstopnochange *
                     (len(sample_radii) - 1)) or count < iterlimitmin):
                if param_cycle > 2 or (parameters[i]["m"] is None
                                       and not fit_superellipse):
                    break
                elif parameters[i]["m"] is None and fit_superellipse:
                    logging.info("%s: Started C fitting at iteration %i" %
                                 (options["ap_name"], count))
                    param_cycle = 3
                    iterstopnochange = max(iterstopnochange, param_cycle)
                    count_nochange = 0
                    count = 0
                    if fit_coefs is None:
                        fit_coefs = (2, 4)
                else:
                    logging.info("%s: Started Fmode fitting at iteration %i" %
                                 (options["ap_name"], count))
                    if fit_superellipse:
                        logging.info("%s: Started C fitting at iteration %i" %
                                     (options["ap_name"], count))
                    param_cycle = base_params + 2 * len(parameters[i]["m"])
                    iterstopnochange = max(iterstopnochange, param_cycle)
                    count_nochange = 0
                    count = 0
                    if fit_coefs is None and not fit_params is None:
                        fit_coefs = fit_params
                        if not 2 in fit_coefs:
                            fit_coefs = tuple(
                                sorted(set([2] + list(fit_coefs))))
                    if not parameters[i]["C"] is None and (
                            not "ap_isofit_losscoefs" in options
                            or options["ap_isofit_losscoefs"] is None):
                        fit_coefs = tuple(sorted(set([4] + list(fit_coefs))))
                    if ("ap_isofit_fitcoefs_FFTinit" in options
                            and options["ap_isofit_fitcoefs_FFTinit"]):
                        for ii in I:
                            isovals = _iso_extract(
                                dat,
                                sample_radii[ii],
                                parameters[ii],
                                use_center,
                                mask=mask,
                                interp_mask=False if mask is None else True,
                                interp_method="bicubic",
                            )

                            if mask is None:
                                coefs = fft(
                                    np.clip(
                                        isovals,
                                        a_max=np.quantile(isovals, 0.85),
                                        a_min=None,
                                    ))
                            else:
                                coefs = fft(
                                    np.clip(
                                        isovals,
                                        a_max=np.quantile(isovals, 0.9),
                                        a_min=None,
                                    ))
                            for m in range(len(parameters[ii]["m"])):
                                parameters[ii]["Am"][m] = np.abs(
                                    coefs[parameters[ii]["m"][m]] /
                                    coefs[0]) * np.sign(
                                        np.angle(
                                            coefs[parameters[ii]["m"][m]]))
                                parameters[ii]["Phim"][m] = np.angle(
                                    coefs[parameters[ii]["m"][m]]) % (2 *
                                                                      np.pi)

        if not (count_nochange <
                (iterstopnochange *
                 (len(sample_radii) - 1)) or count < iterlimitmin):
            break

    logging.info("%s: Completed isohpote fit in %i itterations" %
                 (options["ap_name"], count))
    # Compute errors
    ######################################################################
    ellip_err, pa_err = _FFT_Robust_Errors(
        dat,
        sample_radii,
        parameters,
        use_center,
        results["background noise"],
        mask=mask,
        reg_scale=regularize_scale,
        robust_clip=robust_clip,
        fit_coefs=fit_coefs,
        name=options["ap_name"],
    )
    for i in range(len(ellip)):
        parameters[i]["ellip err"] = ellip_err[i]
        parameters[i]["pa err"] = pa_err[i]
    # Plot fitting results
    ######################################################################
    if "ap_doplot" in options and options["ap_doplot"]:
        Plot_Isophote_Fit(dat, sample_radii, parameters, results, options)

    res = {
        "fit ellip":
        np.array(list(parameters[i]["ellip"] for i in range(len(parameters)))),
        "fit pa":
        np.array(list(parameters[i]["pa"] for i in range(len(parameters)))),
        "fit R":
        sample_radii,
        "fit ellip_err":
        ellip_err,
        "fit pa_err":
        pa_err,
        "auxfile fitlimit":
        "fit limit semi-major axis: %.2f pix" % sample_radii[-1],
    }
    if not fit_params is None:
        res.update({"fit Fmodes": fit_params})
        for m in range(len(fit_params)):
            res.update({
                "fit Fmode A%i" % fit_params[m]:
                np.array(
                    list(parameters[i]["Am"][m]
                         for i in range(len(parameters)))),
                "fit Fmode Phi%i" % fit_params[m]:
                np.array(
                    list(parameters[i]["Phim"][m]
                         for i in range(len(parameters)))),
            })
    if fit_superellipse:
        res.update({
            "fit C":
            np.array(list(parameters[i]["C"] for i in range(len(parameters))))
        })
    return IMG, res