# Copyright 2021 United States Government as represented by the Administrator of the National Aeronautics and Space
# Administration.  No copyright is claimed in the United States under Title 17, U.S. Code. All Other Rights Reserved.


"""
This module provides a class which implements an unresolved center finding RelNav technique along with a new meta class
that adds concrete center-of-brightness to center-of-figure correction methods.

Description of the Technique
----------------------------

Unresolved center finding is applicable when you observe a target that is dominated by the point spread function of the
camera rather than by the geometry of the object.  Typically this occurs when the apparent diameter of the object in
the image is less than 5-10 pixels. Because these targets are dominated by the camera point spread function,
GIANT treats unresolved bodies the same way that stars are treated.  In fact, all of the same algorithms and functions
are used from the :mod:`.image_processing` module as are used for extracting potential star locations from images.

First, the area around the expected location of the target body is searched for all groupings of pixels that exceed a
specified threshold.  Then, as long as there is 1 and only 1 grouping of pixels above the threshold in the search
region, the sub-pixel center of brightness is extracted by using the specified fitting function (typically a 2D gaussian
or a moment algorithm.  This is all done in the :meth:`.ImageProcessing.locate_subpixel_poi_in_roi` method from the
image processing class.

This routine is generally used during early approach to an object, or for extracting Celestial Navigation (CelNav)
observations to known targets from the solar system.  It does not apply once the target's apparent diameter begins to
exceed 5-10 pixels and will begin failing at that point.

Tuning
------

The primary control for tuning this technique is through the tuning of the
:meth:`.ImageProcessing.locate_subpixel_poi_in_roi` method.  There are a number of tuning parameters
for this method and we direct you to its documentation for more details.

In addition, there are a few tuning parameters for the class itself.
The search region is controlled by the :attr:`~.UnresolvedCenterFinding.search_distance` attribute.  This should be an
integer which specifies half of the square region to search around the predicted center, such that a 2*
:attr:`~.UnresolvedCenterFinding.search_distance` by 2* :attr:`~.UnresolvedCenterFinding.search_distance` pixels of the
image will be searched.

In addition, Whether the phase correction is applied or not is controlled by the boolean flag
:attr:`~.UnresolvedCenterFinding.apply_phase_correction`.  The phase correction computation can be controlled using the
:attr:`~.UnresolvedCenterFinding.phase_correction_type` and :attr:`~.UnresolvedCenterFinding.brdf` attributes.

Use
---

This class is not typically not used directly by the user, but instead is
called from the :class:`.RelativeOpNav` class using the technique name of ``unresolved``.  For more details on using
this class directly, refer to the following class documentation.  For more details on using this class through the
:class:`.RelativeOpNav` user interface refer to the :mod:`.relnav_class` documentation.
"""

import warnings

from enum import Enum, auto

from typing import List, Optional, Tuple, Union

from abc import ABC

import numpy as np

from giant.ray_tracer.rays import Rays
from giant.ray_tracer.scene import Scene, SceneObject
from giant.ray_tracer.illumination import IlluminationModel, McEwenIllumination
from giant.camera import Camera
from giant.image import OpNavImage
from giant.image_processing import ImageProcessing

from giant.relative_opnav.estimators.estimator_interface_abc import RelNavEstimator, RelNavObservablesType

from giant._typing import Real


class PhaseCorrectionType(Enum):
    """
    This enumeration provides the possible phase correction methods to use to convert the observed center-of-brightness
    to a center of figure observation.
    """

    SIMPLE = auto()
    """
    This method assumes a spherical target and computes the phase correction for a sphere 
    """

    RASTERED = auto()
    """
    This method assumes a tessellated object and renders it using rasterization to compute the phase correction.
    
    This will be much more accurate than the SIMPLE method for most tessellated targets, especially for ones that are 
    not roughly spherical in shape.  However, it is also much more computationally expensive and the newly realized 
    accuracy could be useless if the object is still very small in the FOV of the camera.
    """


class PhaseCorrector(RelNavEstimator, ABC):
    """
    This class adds phase correction capabilities to RelNavEstimator.

    Phase correction is the process by which we attempt to move an observed center of brightness (the centroid of a
    bright patch in this case) to more closely resemble the location where the center of figure would be observed in the
    image.  This is done by correcting for the fact that the phase angle between the camera, the target, and the sun,
    can cause the observed center of brightness to be biased towards one side of the shape because only part of the
    target appears illuminated.

    Specifically, this class adds 2 new key word argument inputs and attributes :attr:`phase_correction_type`, and
    :attr:`brdf` which specify what phase correction method to use to compute the phase correction, as well as the BRDF
    that is used to generate the illumination information for each facet when the ``RASTERED`` phase correction method
    is used.  In addition, it provides the method :meth:`compute_phase_correction` which will return the phase
    correction based as a size 2 numpy array from the center of brightness to the center of figure in pixels using the
    current scene settings and the selected :attr:`phase_correction_type`.  It also defines 3 helper methods which are
    included for documentation purposes but are rarely interfaced with directly:
    :meth:`compute_line_of_sight_sun_image`, :meth:`simple_phase_correction`, and :meth:`rastered_phase_correction`.

    Generally this class is not used by the user and is instead used internally by the RelNav techniques that the user
    interacts with.  If you are trying to implement a new relnav technique that needs phase correction capabilities,
    then you can subclass this class (no need to also subclass :class:`.RelNavEstimator`) and then use the
    :meth:`compute_phase_correction` method when you need it.  For more information on defining a new RelNav technique,
    refer to the :mod:`.relative_opnav.estimators` documentation.
    """

    def __init__(self, scene: Scene, camera: Camera, image_processing: ImageProcessing,
                 phase_correction_type: Union[PhaseCorrectionType, str] = PhaseCorrectionType.SIMPLE,
                 brdf: Optional[IlluminationModel] = None):
        """
        :param scene: The :class:`.Scene` object containing the target, light, and obscuring objects.
        :param camera: The :class:`.Camera` object containing the camera model and images to be utilized
        :param image_processing: The :class:`.ImageProcessing` object to be used to process the images
        :param phase_correction_type: The type of phase correction to use.  Should be one of the PhaseCorrectionType
                                      enum values
        :param brdf: The illumination model to use to compute the illumination values if the ``RASTERED`` phase
                     correction type is used.  If the ``RASTERED`` phase correction type is not used this is ignored.
                     If this is left as ``None`` and the ``Rastered`` phase correction type is used, this will default
                     to the McEwen Model, :class:`.McEwenIllumination`.
        """

        super().__init__(scene, camera, image_processing)

        self.phase_correction_type: PhaseCorrectionType = phase_correction_type
        """
        The type of phase correction to use, if requested.

        See :class:`.PhaseCorrectionType` for details.
        """

        if isinstance(self.phase_correction_type, str):
            self.phase_correction_type = PhaseCorrectionType[self.phase_correction_type.upper()]

        self.brdf: IlluminationModel = brdf
        """
        The illumination model used to convert geometry into expected illumination.  

        This is only used if the ``RASTERED`` phase correction type is chosen and is ignored otherwise.
        """

        if (self.phase_correction_type is PhaseCorrectionType.RASTERED) and (self.brdf is None):
            self.brdf = McEwenIllumination()

    def simple_phase_correction(self, target_ind: int, target: SceneObject, line_of_sight_sun_image: np.ndarray,
                                temperature: Real) -> np.ndarray:
        """
        This method computes the simple phase correction assuming the target is a sphere.

        First, the apparent diameter of the target in pixels is computed using
        :meth:`.SceneObject.get_apparent_diameter`. Then the phase angle is computed using meth:`.Scene.phase_angle`
        for the target.  Finally, the correction magnitude is computed assuming a McEwen like scattering law.  The
        magnitude is then multiplied by the sun direction vector to compute the phase correction.

        This is based on the technique in https://www.aanda.org/articles/aa/pdf/2004/10/aah4644.pdf

        :param target_ind: the index of the target in the :attr:`.Scene.target_objs` list
        :param target: The target object itself to compute the phase correction for
        :param line_of_sight_sun_image: The unit vector from the sun to the target in the image
        :param temperature: the temperature of the camera when the image was captured
        :return: The phase correction as a length 2 numpy array from the center of brightness to the center of figure
        """

        # get the apparent radius of the target
        radius_pixels = target.get_apparent_diameter(self.camera.model,
                                                     temperature=temperature) / 2

        # get the phase angle between the camera, target, and sun
        phase = self.scene.phase_angle(target_ind)

        if phase < np.pi/9:
            lambertian_correction = radius_pixels*3/8*phase
            lommel_seeliger_correction = radius_pixels*radius_pixels/3
        else:
            lambertian_correction = radius_pixels*3*np.pi/16*(1+np.cos(phase))/((np.pi-phase)*(1/np.tan(phase)) + 1)
            lommel_seeliger_correction = (radius_pixels*2*(np.sin(phase)+(np.pi-phase)*np.cos(phase)) /
                                          (2*np.pi/np.tan(phase/2)-np.sin(phase/2*np.log(1/np.tan(phase/4)))))

        # use the average between lommel_seeliger and lambertian
        correction_mag = (lambertian_correction + lommel_seeliger_correction)/2

        # apply the correction along the sun direction vector
        correction = line_of_sight_sun_image * correction_mag

        return correction

    def rastered_phase_correction(self, target_ind: int, target: SceneObject, temperature: Real):
        """
        This method computes the phase correction by raster rendering the target to determine the offset from the center
        of illumination to the center of figure.

        This method is only applicable to targets that are represented by tesselation, such as triangles or
        parallelograms.  It will in general be more accurate for tessellated bodies than the simple technique, it is
        also much more computationally efficient, and many times, especially when the target is still very small in the
        image, the added accuracy is overwhelmed by the uncertainty of identifying the center of brightness in the
        image.  In addition, if your shape model is very far off from the actual shape, then this will be just as
        inaccurate as using the simple technique, and in some cases perhaps more inaccurate.

        The specific steps to computing the correction using this technique are as follows.  First, the facets of the
        tesselation are each "rendered" assuming no occlusion or shadowing based solely on the incidence and view
        angles.  Then, the "center of brightness" is computed in the camera frame using a moment
        algorithm.  Finally, this center of brightness is projected onto the image, and the difference between it and
        the projected center of figure is the correction vector.

        :param target_ind: the index of the target in the :attr:`.Scene.target_objs` list
        :param target: The target object itself to compute the phase correction for
        :param temperature: the temperature of the camera when the image was captured
        :return: The phase correction as a length 2 numpy array from the center of brightness to the center of figure
        """

        # get the rasterized illumination
        brightness, centers, vertices = self._scene.raster_render(target_ind, self.brdf)

        illuminated_facets = brightness > 0

        # get the area
        illuminated_sides = target.shape.sides[illuminated_facets]
        # compute the norm of the cross product of the sides for the triangles divided by 2
        areas = np.linalg.norm(np.cross(illuminated_sides[..., 0], illuminated_sides[..., 1]), axis=-1)/2

        # compute the photo-center offset in the camera frame
        facet_brightness = areas*brightness[illuminated_facets]
        photo_center_offset = (facet_brightness*centers[illuminated_facets]).sum(axis=0)/facet_brightness.sum()

        # project the photo center into the camera
        image_photo_center = self._camera.model.project_onto_image(photo_center_offset, temperature=temperature)

        # compute and return the offset from the photo center to the center of figure
        return self.computed_bearings[target_ind] - image_photo_center

    def compute_line_of_sight_sun_image(self, target: SceneObject) -> np.ndarray:
        # get the line of sight from the sun in the image
        line_of_sight_sun = target.position-self.scene.light_obj.position.ravel()
        line_of_sight_sun /= np.linalg.norm(line_of_sight_sun)
        line_of_sight_sun_image = self.camera.model.project_directions(line_of_sight_sun)

        return line_of_sight_sun_image

    def compute_phase_correction(self, target_ind: int, target: SceneObject, temperature: Real,
                                 line_of_sight_sun_image: Optional[np.ndarray] = None):
        """
        The method computes the phase correction assuming a spherical target.

        The phase correction attempts to move the observed center-of-brightness measurement to be
        closer to what the actual observed center-of-figure of the object should be (akin to what you would receive from
        something like cross correlation).

        This is done by either assuming a spherical target, computing the apparent diameter of the assumed spherical
        target, and then computing the phase correction in the sun direction using a predefined model of phase shift for
        a spherical target, or by using rasterization to render the target and computing the shift in the center of
        brightness to the center of figure using the rasterized illumination data.  Which technique is used is set by
        the :attr:`phase_correction_type`.  The simple phase correction is performed using
        :meth:`simple_phase_correction` while the more accurate phase correction is performed using
        :meth:`rastered_phase_correction`.

        :param target_ind: The index of the target in the :attr:`.Scene.target_objs` attribute we are considering
        :param target: The actual target object we are considering.  The :attr:`.SceneObject.shape` attribute of this
                       object should be a :class:`.Surface` if the raster method is chosen.
        :param temperature: The temperature of the camera at the time the image was captured.  This is used for
                            projecting points into the camera
        :param line_of_sight_sun_image: The line of sight from the sun towards the target in the image.  This is
                                        essentially the slope of the line from the target to the sun projected into the
                                        image.  This should be a length 2 array or ``None``.  If ``None`` it will be
                                        computed from the scene if required.
        :return: The phase correction as a length 2 numpy array which goes from the observed center of brightness to
                 what the center of figure should be.
        """

        if self.phase_correction_type is PhaseCorrectionType.SIMPLE:
            if line_of_sight_sun_image is None:
                line_of_sight_sun_image = self.compute_line_of_sight_sun_image(target)
            return self.simple_phase_correction(target_ind, target, line_of_sight_sun_image, temperature)

        else:
            return self.rastered_phase_correction(target_ind, target, temperature)


class UnresolvedCenterFinding(PhaseCorrector):
    """
    This class implements GIANT's version of unresolved center finding for extracting bearing measurements to unresolved
    targets in an image.

    The class provides an interface to perform unresolved center finding for each
    target body that is predicted to be in an image.  It does this by looping through each target object contained
    in the :attr:`.Scene.target_objs` attribute.  For each of these targets the algorithm:

    #. Predicts the location of that target in the image using the a priori knowledge
    #. Searches the region around the predicted location defined by the :attr:`.search_distance` attribute for
       bright spots
    #. If only one bright spot exists in the region then it finds the sub-pixel center of the bright spot or, if
       more than one or no bright spots exist in the region it raises a warning and moves on to the next object.
    #. If requested, the observed center-of-brightness is corrected to be closer to what the observed center of
       figure should be using the phase angle, the illumination direction, and the predicted apparent diameter of
       the target in pixels.

    Steps 2 and 3 are both performed by the :meth:`.ImageProcessing.locate_subpixel_poi_in_roi` method, which is the
    same method used to extract potential star locations from an image in the :mod:`.stellar_opnav` package.  Therefore,
    all of the same settings are used to adjust the performance and the reader is directed to to the
    :meth:`.ImageProcessing.locate_subpixel_poi_in_roi` documentation for more information.

    The search region is controlled by the :attr:`search_distance` attribute.  This should be an integer which
    specifies half of the square region to search around the predicted center, such that a 2* :attr:`search_distance` by
    2* :attr:`search_distance` pixels of the image will be searched.  Whether the phase correction is applied or not is
    controlled by the boolean flag :attr:`apply_phase_correction`.

    When all of the required data has been successfully loaded into an instance of this class, the :meth:`estimate`
    method is used to extract the sub-pixel centers of the target bodies predicted to be in the requested image.  The
    results are stored into the :attr:`observed_bearings` attribute. In addition, the predicted location for each target
    is stored in the :attr:`computed_bearings` attribute. Finally, the details about the fit are stored as a
    dictionary in the appropriate element in the :attr:`details` attribute.  Specifically, these dictionaries will
    contain the following keys.

    ====================== =============================================================================================
    Key                    Description
    ====================== =============================================================================================
    ``'PSF'``              The fit PSF values.  Only available if successful.  Will be ``None`` if
                           :attr:`.ImageProcessing.save_psf` is ``False``
    ``'Phase Correction'`` The phase correction vector used to convert from center of brightness to center of figure.
                           This will only be available if the fit was successful.  If :attr:`apply_phase_correction` is
                           ``False`` then this will be an array of 0.
    ``'SNR'``              The peak signal to noise ratio of the detection.  This will only be set if the fit was
                           successful.  If :attr:`.ImageProcessing.return_stats` is ``False`` then this will be
                           ``None``.
    ``'Max Intensity'``    The intensity of the peak pixel used in the PSF fit.  This will only be set if the fit was
                           successful.
    ``'Failed'``           A message indicating why the fit failed.  This will only be present if the fit failed (so you
                           could do something like ``'Failed' in unresolved.details[target_ind]`` to check if something
                           failed.  The message should be a human readable description of what called the failure
    ``'Found Results'``    The points of interest that were found in the search region.  This is only present if the fit
                           failed because there were more than 1 point of interest in the search region.  The value to
                           this key is the return from :meth:`.ImageProcessing.locate_subpixel_poi_in_roi`
    ====================== =============================================================================================

    .. warning::
        Before calling the :meth:`.estimate` method be sure that the scene has been updated to correspond to the correct
        image time.  This class does not update the scene automatically.
    """

    technique: str = 'unresolved'
    """
    The name of the technique identifier in the :class:`.RelativeOpNav` class.
    """

    observable_type: List[RelNavObservablesType] = [RelNavObservablesType.CENTER_FINDING]
    """
    The type of observables this technique generates.
    """

    def __init__(self, scene: Scene, camera: Camera, image_processing: ImageProcessing,
                 search_distance: int = 15, apply_phase_correction: bool = False,
                 phase_correction_type: Union[PhaseCorrectionType, str] = PhaseCorrectionType.SIMPLE,
                 brdf: Optional[IlluminationModel] = None):
        """
        :param scene: The :class:`.Scene` object containing the target, light, and obscuring objects.
        :param camera: The :class:`.Camera` object containing the camera model and images to be utilized
        :param image_processing: The :class:`.ImageProcessing` object to be used to process the images
        :param search_distance: The search radius to search around the predicted centers for the observed centers of
                                the target objects
        :param apply_phase_correction: A boolean flag specifying whether to apply the phase correction to the observed
                                       center of brightness to get closer to the center of figure based on the predicted
                                       apparent diameter of the object.
        :param phase_correction_type: The type of phase correction to use.  Should be one of the PhaseCorrectionType
                                      enum values
        :param brdf: The illumination model to use to compute the illumination values if the ``RASTERED`` phase
                     correction type is used.  If the ``RASTERED`` phase correction type is not used this is ignored.
                     If this is left as ``None`` and the ``Rastered`` phase correction type is used, this will default
                     to the McEwen Model, :class:`.McEwenIllumination`.
        """

        super().__init__(scene, camera, image_processing, phase_correction_type=phase_correction_type, brdf=brdf)

        self.search_distance: int = int(search_distance)
        """
        Half of the distance to search around the predicted centers for the observed centers of the target objects in 
        pixels.
        """

        self.apply_phase_correction: bool = apply_phase_correction
        """
        A boolean flag specifying whether to apply the phase correction or not
        """


    def estimate(self, image: OpNavImage, include_targets: Optional[List[bool]] = None):
        """
        This method extracts the observed sub-pixel centers for each requested target object from the supplied image.

        The method works by first predicting the center of the target objects, then searching for bright spots around
        the predicted centers, and finally identifying the subpixel centers of the bright spots.  For a more in depth
        discussion refer to the :class:.UnresolvedCenterFinding` documentation.  The results are stored into the
        :attr:`computed_bearings`, :attr:`observed_bearings`, and :attr:`details` attributes. If a target object
        cannot be matched to an observed bright spot then a warning is printed and NaN values are stored.

        .. warning::
            Before calling this method be sure that the scene has been updated to correspond to the correct
            image time.  This method does not update the scene automatically.

        :param image: The image the unresolved algorithm should be applied to as an OpNavImage
        :param include_targets: An argument specifying which targets should be processed for this image.  If ``None``
                                then all are processed (no, the irony is not lost on me...)
        """

        # process each requested target
        for target_ind, target in self.target_generator(include_targets):

            # store the relative position
            relative_position = target.position.ravel()

            # predict where the target should be
            self.computed_bearings[target_ind] = self.camera.model.project_onto_image(relative_position,
                                                                                      temperature=image.temperature)

            # check if this is being obscured by anything in the scene so we can't see it
            # also store the closest other object in the scene in the image

            # make the ray that starts at the target and points to the camera
            vis_ray = Rays(relative_position.ravel(),
                           -relative_position.ravel() / np.linalg.norm(relative_position))

            # trace a ray from the object to the sun.  If we strike something the object is shadowed
            shad_dir = self.scene.light_obj.shape.location.ravel() - relative_position.ravel()
            shad_ray = Rays(relative_position.ravel(), shad_dir / np.linalg.norm(shad_dir))

            if self.scene.obscuring_objs is not None:
                possible_obscurers = self.scene.target_objs + self.scene.obscuring_objs
            else:
                possible_obscurers = self.scene.target_objs

            stop_processing: bool = False  # a flag specifying whether we need to stop because this is obscured

            # the closest distance from this target to any other object in the scene in pixels
            closest_other_distance: float = float(max(image.shape))*100

            for obscurer in possible_obscurers:
                if obscurer is target:
                    continue

                # trace a ray from the target to the camera to see if we strike anything on the way
                ores = obscurer.shape.trace(vis_ray)

                if ores["check"].any():
                    print('Target is obscured', flush=True)
                    self.observed_bearings[target_ind] = np.array([np.nan, np.nan])
                    stop_processing = True
                    self.details[target_ind] = {'Failed': 'target is obscured'}
                    break

                # trace a ray from the target to the sun to see if we strike anything
                shad_res = obscurer.shape.trace(shad_ray)

                if shad_res["check"].any():
                    print('Target is shadowed', flush=True)
                    self.observed_bearings[target_ind] = np.array([np.nan, np.nan])
                    self.details[target_ind] = {'Failed': 'target is shadowed'}
                    stop_processing = True
                    break

                image_distance = np.linalg.norm(self.camera.model.project_onto_image(obscurer.position.ravel(),
                                                                                     temperature=image.temperature) -
                                                self.computed_bearings[target_ind])

                closest_other_distance = min(closest_other_distance, image_distance)

            # if the target was obscured or shadowed skip it
            if stop_processing:
                continue

            if closest_other_distance < self.search_distance:
                warnings.warn(f"The target is with {closest_other_distance} pixels of another object in the scene but "
                              f"the search distance is {self.search_distance}")

            # determine the pixels we will search for the target in
            lr_search = self.computed_bearings[target_ind][0].round().astype(int) + np.arange(-self.search_distance,
                                                                                              self.search_distance)
            ud_search = self.computed_bearings[target_ind][1].round().astype(int) + np.arange(-self.search_distance,
                                                                                              self.search_distance)
            # check that we are within the field of view for our search region
            top_bound = ud_search < 0
            bottom_bound = ud_search >= image.shape[0]
            left_bound = lr_search < 0
            right_bound = lr_search >= image.shape[1]
            fov_test = top_bound | left_bound | bottom_bound | right_bound

            if fov_test.all():
                print('target is outside of FOV', flush=True)
                self.observed_bearings[target_ind] = np.array([np.nan, np.nan])
                self.details[target_ind] = {'Failed': 'target search region is completely outside of FOV'}
                continue

            # build the region of interest to search
            # noinspection PyTypeChecker
            roi: Tuple[np.ndarray, np.ndarray] = np.meshgrid(lr_search[~fov_test], ud_search[~fov_test])[::-1]

            # use the star centroiding to find the pixel level location of the object
            # note that this will call denoise_image for us if it is turned on
            res = self.image_processing.locate_subpixel_poi_in_roi(image, roi)

            # parse the output
            if self.image_processing.save_psf:
                if self.image_processing.return_stats:
                    points, illums, psfs, _, snrs = res
                else:
                    points, illums, psfs = res
                    snrs = None
            else:
                if self.image_processing.return_stats:
                    points, illums, _, snrs = res
                else:
                    points, illums = res
                    snrs = None
                psfs = None

            points = points.T

            if len(points) > 1:
                # TODO: consider picking the closest point to the a priori in this instance and throwing a warning
                warnings.warn("too many points of interest found in the search region for epoch: {0} target: {1}.\n"
                              "Please adjust your search distance/poi "
                              "size or manually specify the poi\n".format(image.observation_date, target_ind))
                self.observed_bearings[target_ind] = np.array([np.nan, np.nan])
                self.details[target_ind] = {'Failed': 'Too many points of interested found in search region',
                                            'found_results': res}
                continue

            elif len(points) == 0:
                warnings.warn("No points of interest found in the search region for epoch: {0} obj: {1}.\n"
                              "Please adjust your search distance/poi "
                              "size or manually specify the poi\n".format(image.observation_date,
                                                                          target_ind))
                self.observed_bearings[target_ind] = np.array([np.nan, np.nan])
                self.details[target_ind] = {'Failed': 'No points of interest found in search region'}

                continue

            elif np.isfinite(points).all():

                # store the found center
                self.observed_bearings[target_ind] = points.ravel()

                # if we are applying the phase correction, compute it
                if self.apply_phase_correction:
                    correction = self.compute_phase_correction(target_ind, target, image.temperature)

                    self.observed_bearings[target_ind] += correction
                else:
                    correction = np.zeros(2, dtype=np.float64)

                self.details[target_ind] = {'PSF': psfs,
                                            'Phase Correction': correction,
                                            'SNR': snrs,
                                            'Max intensity': illums}

            else:
                warnings.warn('unable to locate subpixel center for epoch: {0} obj: {1}'.format(image.observation_date,
                                                                                                target_ind))
                self.details[target_ind] = {'Failed': "Bad PSF fit"}

                self.observed_bearings[target_ind] = np.array([np.nan, np.nan])
                continue
