Esempio n. 1
0
def getThresholdedStripe3DPoints(config, img_handle, frame_min, frame_max, rho, theta, mask, flat_struct, \
    dark, stripe_width_factor=1.0, centroiding=False, point1=None, point2=None, debug=False):
    """ Threshold the image and get a list of pixel positions and frames of threshold passers. 
        This function handles all input types of data.

    Arguments;
        config: [config object] configuration object (loaded from the .config file).
        img_handle: [FrameInterface instance] Object which has a common interface to various input files.
        frame_min: [int] First frame to process.
        frame_max: [int] Last frame to process.
        rho: [float] Line distance from the center in HT space (pixels).
        theta: [float] Angle in degrees in HT space.
        mask: [ndarray] Image mask.
        flat_struct: [Flat struct] Structure containing the flat field. None by default.
        dark: [ndarray] Dark frame.

    Keyword arguments:
        stripe_width_factor: [float] Multipler by which the default stripe width will be multiplied. Default
            is 1.0
        centroiding: [bool] If True, the indices will be returned in the centroiding mode, which means
            that point1 and point2 arguments must be given.
        point1: [list] (x, y, frame) Of the first reference point of the detection.
        point2: [list] (x, y, frame) Of the second reference point of the detection.
        debug: [bool] If True, extra debug messages and plots will be shown.
    
    Return:
        xs, ys, zs: [tuple of lists] Indices of (x, y, frame) of threshold passers for every frame.
    """

    # Get indices of stripe pixels around the line of the meteor
    img_h, img_w = img_handle.ff.maxpixel.shape
    stripe_indices = getStripeIndices(
        rho, theta, stripe_width_factor * config.stripe_width, img_h, img_w)

    # If centroiding should be done, prepare everything for cutting out parts of the image for photometry
    if centroiding:

        # Compute the unit vector which describes the motion of the meteor in the image domain
        point1 = np.array(point1)
        point2 = np.array(point2)
        motion_vect = point2[:2] - point1[:2]
        motion_vect_unit = vectNorm(motion_vect)

        # Get coordinates of 2 points that describe the line
        x1, y1, z1 = point1
        x2, y2, z2 = point2

        # Compute the average angular velocity in px per frame
        ang_vel = np.sqrt((x2 - x1)**2 + (y2 - y1)**2) / (z2 - z1)

        # Compute the vector describing the length and direction of the meteor per frame
        motion_vect = ang_vel * motion_vect_unit

    # If the FF files is given, extract the points from FF after threshold
    if img_handle.input_type == 'ff':

        # Threshold the FF file
        img_thres = Image.thresholdFF(img_handle.ff, config.k1_det, config.j1_det, mask=mask, \
            mask_ave_bright=False)

        # Extract the thresholded image by min and max frames from FF file
        img = selectFFFrames(np.copy(img_thres), img_handle.ff, frame_min,
                             frame_max)

        # Remove lonely pixels
        img = morph.clean(img)

        # Extract the stripe from the thresholded image
        stripe = np.zeros(img.shape, img.dtype)
        stripe[stripe_indices] = img[stripe_indices]

        # Show stripe
        # show2("stripe", stripe*255)

        # Show 3D could
        # show3DCloud(ff, stripe)

        # Get stripe positions (x, y, frame)
        stripe_positions = stripe.nonzero()
        xs = stripe_positions[1]
        ys = stripe_positions[0]
        zs = img_handle.ff.maxframe[stripe_positions]

        return xs, ys, zs

    # If video frames are available, extract indices on all frames in the given range
    else:

        xs_array = []
        ys_array = []
        zs_array = []

        # Go through all frames in the frame range
        for fr in range(frame_min, frame_max + 1):

            # Break the loop if outside frame size
            if fr == (img_handle.total_frames - 1):
                break

            # Set the frame number
            img_handle.setFrame(fr)

            # Load the frame
            fr_img = img_handle.loadFrame()

            # Apply the dark frame
            if dark is not None:
                fr_img = Image.applyDark(fr_img, dark)

            # Apply the flat to frame
            if flat_struct is not None:
                fr_img = Image.applyFlat(fr_img, flat_struct)

            # Mask the image
            fr_img = MaskImage.applyMask(fr_img, mask)

            # Threshold the frame
            img_thres = Image.thresholdImg(fr_img, img_handle.ff.avepixel, img_handle.ff.stdpixel, \
                config.k1_det, config.j1_det, mask=mask, mask_ave_bright=False)

            # Remove lonely pixels
            img_thres = morph.clean(img_thres)

            # Extract the stripe from the thresholded image
            stripe = np.zeros(img_thres.shape, img_thres.dtype)
            stripe[stripe_indices] = img_thres[stripe_indices]

            # Include more pixels for centroiding and photometry and mask out per frame pixels
            if centroiding:

                # Dilate the pixels in the stripe twice, to include more pixels for photometry
                stripe = morph.dilate(stripe)
                stripe = morph.dilate(stripe)

                # Get indices of the stripe that is perpendicular to the meteor, and whose thickness is the
                # length of the meteor on this particular frame - this is called stripe_indices_motion

                # Compute the previous, current, and the next linear model position of the meteor on the
                #   image
                model_pos_prev = point1[:2] + (fr - 1 - z1) * motion_vect
                model_pos = point1[:2] + (fr - z1) * motion_vect
                model_pos_next = point1[:2] + (fr + 1 - z1) * motion_vect

                # Get the rho, theta of the line perpendicular to the meteor line
                x_inters, y_inters = model_pos

                # Check if the previous, current or the next centroids are outside bounds, and if so, skip the
                #   frame
                if (not checkCentroidBounds(model_pos_prev, img_w, img_h)) or \
                    (not checkCentroidBounds(model_pos, img_w, img_h)) or \
                    (not checkCentroidBounds(model_pos_next, img_w, img_h)):

                    continue

                # Get parameters of the perpendicular line to the meteor line
                rho2, theta2 = htLinePerpendicular(rho, theta, x_inters,
                                                   y_inters, img_h, img_w)

                # Compute the image indices of this position which will be the intersection with the stripe
                #   The width of the line will be 2x the angular velocity
                stripe_length = 6 * ang_vel
                if stripe_length < stripe_width_factor * config.stripe_width:
                    stripe_length = stripe_width_factor * config.stripe_width
                stripe_indices_motion = getStripeIndices(
                    rho2, theta2, stripe_length, img_h, img_w)

                # Mark only those parts which overlap both lines, which effectively creates a mask for
                #    photometry an centroiding, excluding other influences
                stripe_new = np.zeros_like(stripe)
                stripe_new[stripe_indices_motion] = stripe[
                    stripe_indices_motion]
                stripe = stripe_new

                if debug:

                    # Show the extracted stripe
                    img_stripe = np.zeros_like(stripe)
                    img_stripe[stripe_indices] = 1
                    final_stripe = np.zeros_like(stripe)
                    final_stripe[stripe_indices_motion] = img_stripe[
                        stripe_indices_motion]

                    plt.imshow(final_stripe)
                    plt.show()

            if debug and centroiding:

                print(fr)
                print('mean stdpixel3:', np.mean(img_handle.ff.stdpixel))
                print('mean avepixel3:', np.mean(img_handle.ff.avepixel))
                print('mean frame:', np.mean(fr_img))
                fig, (ax1, ax2) = plt.subplots(nrows=2,
                                               sharex=True,
                                               sharey=True)

                fr_img_noavg = Image.applyDark(fr_img, img_handle.ff.avepixel)
                #fr_img_noavg = fr_img

                # Auto levels
                min_lvl = np.percentile(fr_img_noavg[2:, :], 1)
                max_lvl = np.percentile(fr_img_noavg[2:, :], 99.0)

                # Adjust levels
                fr_img_autolevel = Image.adjustLevels(fr_img_noavg, min_lvl,
                                                      1.0, max_lvl)

                ax1.imshow(stripe, cmap='gray')
                ax2.imshow(fr_img_autolevel, cmap='gray')
                plt.show()

                pass

            # Get stripe positions (x, y, frame)
            stripe_positions = stripe.nonzero()
            xs = stripe_positions[1]
            ys = stripe_positions[0]
            zs = np.zeros_like(xs) + fr

            # Add the points to the list
            xs_array.append(xs)
            ys_array.append(ys)
            zs_array.append(zs)

            if debug:
                print('---')
                print(stripe.nonzero())
                print(xs, ys, zs)

        if len(xs_array) > 0:

            # Flatten the arrays
            xs_array = np.concatenate(xs_array)
            ys_array = np.concatenate(ys_array)
            zs_array = np.concatenate(zs_array)

        else:
            xs_array = np.array(xs_array)
            ys_array = np.array(ys_array)
            zs_array = np.array(zs_array)

        return xs_array, ys_array, zs_array
Esempio n. 2
0
def extractStars(ff_dir,
                 ff_name,
                 config=None,
                 max_global_intensity=150,
                 border=10,
                 neighborhood_size=10,
                 intensity_threshold=5,
                 flat_struct=None,
                 dark=None,
                 mask=None):
    """ Extracts stars on a given FF bin by searching for local maxima and applying PSF fit for star 
        confirmation.

        Source of one part of the code: 
    http://stackoverflow.com/questions/9111711/get-coordinates-of-local-maxima-in-2d-array-above-certain-value
    
    Arguments:
        ff_dir: [str] Path to directory where FF files are.
        ff_name: [str] Name of the FF file.
        config: [config object] configuration object (loaded from the .config file)
        max_global_intensity: [int] maximum mean intensity of an image before it is discared as too bright
        border: [int] apply a mask on the detections by removing all that are too close to the given image 
            border (in pixels)
        neighborhood_size: [int] size of the neighbourhood for the maximum search (in pixels)
        intensity_threshold: [float] a threshold for cutting the detections which are too faint (0-255)
        flat_struct: [Flat struct] Structure containing the flat field. None by default.
        dark: [ndarray] Dark frame. None by default.
        mask: [ndarray] Mask image. None by default.

    Return:
        x2, y2, background, intensity, fwhm: [list of ndarrays]
            - x2: X axis coordinates of the star
            - y2: Y axis coordinates of the star
            - background: background intensity
            - intensity: intensity of the star
            - Gaussian Full width at half maximum (FWHM) of fitted stars
    """

    # This will be returned if there was an error
    error_return = [[], [], [], [], [], []]

    # Load parameters from config if given
    if config:
        max_global_intensity = config.max_global_intensity
        border = config.border
        neighborhood_size = config.neighborhood_size
        intensity_threshold = config.intensity_threshold

    # Load the FF bin file
    ff = FFfile.read(ff_dir, ff_name)

    # If the FF file could not be read, skip star extraction
    if ff is None:
        return error_return

    # Apply the dark frame
    if dark is not None:
        ff.avepixel = Image.applyDark(ff.avepixel, dark)

    # Apply the flat
    if flat_struct is not None:
        ff.avepixel = Image.applyFlat(ff.avepixel, flat_struct)

    # Mask the FF file
    if mask is not None:
        ff = MaskImage.applyMask(ff, mask, ff_flag=True)

    # Calculate image mean and stddev
    global_mean = np.mean(ff.avepixel)

    # Check if the image is too bright and skip the image
    if global_mean > max_global_intensity:
        return error_return

    data = ff.avepixel.astype(np.float32)

    # Apply a mean filter to the image to reduce noise
    data = ndimage.filters.convolve(data, weights=np.full((2, 2), 1.0 / 4))

    # Locate local maxima on the image
    data_max = filters.maximum_filter(data, neighborhood_size)
    maxima = (data == data_max)
    data_min = filters.minimum_filter(data, neighborhood_size)
    diff = ((data_max - data_min) > intensity_threshold)
    maxima[diff == 0] = 0

    # Apply a border mask
    border_mask = np.ones_like(maxima) * 255
    border_mask[:border, :] = 0
    border_mask[-border:, :] = 0
    border_mask[:, :border] = 0
    border_mask[:, -border:] = 0
    maxima = MaskImage.applyMask(maxima, border_mask, image=True)

    # Remove all detections close to the mask image
    if mask is not None:
        erosion_kernel = np.ones((5, 5), mask.img.dtype)
        mask_eroded = cv2.erode(mask.img, erosion_kernel, iterations=1)

        maxima = MaskImage.applyMask(maxima, mask_eroded, image=True)

    # Find and label the maxima
    labeled, num_objects = ndimage.label(maxima)

    # Skip the image if there are too many maxima to process
    if num_objects > config.max_stars:
        print('Too many candidate stars to process! {:d}/{:d}'.format(
            num_objects, config.max_stars))
        return error_return

    # Find centres of mass of each labeled objects
    xy = np.array(
        ndimage.center_of_mass(data, labeled, range(1, num_objects + 1)))

    # Remove all detection on the border
    #xy = xy[np.where((xy[:, 1] > border) & (xy[:,1] < ff.ncols - border) & (xy[:,0] > border) & (xy[:,0] < ff.nrows - border))]

    # Unpack star coordinates
    y, x = np.hsplit(xy, 2)

    # # Plot stars before the PSF fit
    # plotStars(ff, x, y)

    # Fit a PSF to each star
    x2, y2, amplitude, intensity, sigma_y_fitted, sigma_x_fitted = fitPSF(
        ff, global_mean, x, y, config)

    # x2, y2, amplitude, intensity = list(x), list(y), [], [] # Skip PSF fit

    # # Plot stars after PSF fit filtering
    # plotStars(ff, x2, y2)

    # Compute FWHM from one dimensional sigma
    sigma_x_fitted = np.array(sigma_x_fitted)
    sigma_y_fitted = np.array(sigma_y_fitted)
    sigma_fitted = np.sqrt(sigma_x_fitted**2 + sigma_y_fitted**2)
    fwhm = 2.355 * sigma_fitted

    return ff_name, x2, y2, amplitude, intensity, fwhm
def getThresholdedStripe3DPoints(config, img_handle, frame_min, frame_max, rho, theta, mask, flat_struct, \
    dark, stripe_width_factor=1.0, centroiding=False, point1=None, point2=None, debug=False):
    """ Threshold the image and get a list of pixel positions and frames of threshold passers. 
        This function handles all input types of data.

    Arguments;
        config: [config object] configuration object (loaded from the .config file).
        img_handle: [FrameInterface instance] Object which has a common interface to various input files.
        frame_min: [int] First frame to process.
        frame_max: [int] Last frame to process.
        rho: [float] Line distance from the center in HT space (pixels).
        theta: [float] Angle in degrees in HT space.
        mask: [ndarray] Image mask.
        flat_struct: [Flat struct] Structure containing the flat field. None by default.
        dark: [ndarray] Dark frame.

    Keyword arguments:
        stripe_width_factor: [float] Multipler by which the default stripe width will be multiplied. Default
            is 1.0
        centroiding: [bool] If True, the indices will be returned in the centroiding mode, which means
            that point1 and point2 arguments must be given.
        point1: [list] (x, y, frame) Of the first reference point of the detection.
        point2: [list] (x, y, frame) Of the second reference point of the detection.
        debug: [bool] If True, extra debug messages and plots will be shown.
    
    Return:
        xs, ys, zs: [tuple of lists] Indices of (x, y, frame) of threshold passers for every frame.
    """


    # Get indices of stripe pixels around the line of the meteor
    img_h, img_w = img_handle.ff.maxpixel.shape
    stripe_indices = getStripeIndices(rho, theta, stripe_width_factor*config.stripe_width, img_h, img_w)

    # If centroiding should be done, prepare everything for cutting out parts of the image for photometry
    if centroiding:

        # Compute the unit vector which describes the motion of the meteor in the image domain
        point1 = np.array(point1)
        point2 = np.array(point2)
        motion_vect = point2[:2] - point1[:2]
        motion_vect_unit = vectNorm(motion_vect)

        # Get coordinates of 2 points that describe the line
        x1, y1, z1 = point1
        x2, y2, z2 = point2

        # Compute the average angular velocity in px per frame
        ang_vel = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)/(z2 - z1)

        # Compute the vector describing the length and direction of the meteor per frame
        motion_vect = ang_vel*motion_vect_unit


    # If the FF files is given, extract the points from FF after threshold
    if img_handle.input_type == 'ff':

        # Threshold the FF file
        img_thres = Image.thresholdFF(img_handle.ff, config.k1_det, config.j1_det, mask=mask, \
            mask_ave_bright=False)

        # Extract the thresholded image by min and max frames from FF file
        img = selectFFFrames(np.copy(img_thres), img_handle.ff, frame_min, frame_max)

        # Remove lonely pixels
        img = morph.clean(img)

        # Extract the stripe from the thresholded image
        stripe = np.zeros(img.shape, img.dtype)
        stripe[stripe_indices] = img[stripe_indices]

        # Show stripe
        # show2("stripe", stripe*255)

        # Show 3D could
        # show3DCloud(ff, stripe)

        # Get stripe positions (x, y, frame)
        stripe_positions = stripe.nonzero()
        xs = stripe_positions[1]
        ys = stripe_positions[0]
        zs = img_handle.ff.maxframe[stripe_positions]

        return xs, ys, zs


    # If video frames are available, extract indices on all frames in the given range
    else:

        xs_array = []
        ys_array = []
        zs_array = []

        # Go through all frames in the frame range
        for fr in range(frame_min, frame_max + 1):


            # Break the loop if outside frame size
            if fr == (img_handle.total_frames - 1):
                break

            # Set the frame number
            img_handle.setFrame(fr)

            # Load the frame
            fr_img = img_handle.loadFrame()


            # Apply the dark frame
            if dark is not None:
                fr_img = Image.applyDark(fr_img, dark)

            # Apply the flat to frame
            if flat_struct is not None:
                fr_img = Image.applyFlat(fr_img, flat_struct)

            # Mask the image
            fr_img = MaskImage.applyMask(fr_img, mask)
                

            # Threshold the frame
            img_thres = Image.thresholdImg(fr_img, img_handle.ff.avepixel, img_handle.ff.stdpixel, \
                config.k1_det, config.j1_det, mask=mask, mask_ave_bright=False)


            # Remove lonely pixels
            img_thres = morph.clean(img_thres)

            # Extract the stripe from the thresholded image
            stripe = np.zeros(img_thres.shape, img_thres.dtype)
            stripe[stripe_indices] = img_thres[stripe_indices]


            # Include more pixels for centroiding and photometry and mask out per frame pixels
            if centroiding:
                
                # Dilate the pixels in the stripe twice, to include more pixels for photometry
                stripe = morph.dilate(stripe)
                stripe = morph.dilate(stripe)

                # Get indices of the stripe that is perpendicular to the meteor, and whose thickness is the 
                # length of the meteor on this particular frame - this is called stripe_indices_motion

                # Compute the previous, current, and the next linear model position of the meteor on the 
                #   image
                model_pos_prev = point1[:2] + (fr - 1 - z1)*motion_vect
                model_pos = point1[:2] + (fr - z1)*motion_vect
                model_pos_next = point1[:2] + (fr + 1 - z1)*motion_vect

                # Get the rho, theta of the line perpendicular to the meteor line
                x_inters, y_inters = model_pos

                # Check if the previous, current or the next centroids are outside bounds, and if so, skip the
                #   frame
                if (not checkCentroidBounds(model_pos_prev, img_w, img_h)) or \
                    (not checkCentroidBounds(model_pos, img_w, img_h)) or \
                    (not checkCentroidBounds(model_pos_next, img_w, img_h)):

                    continue

                # Get parameters of the perpendicular line to the meteor line
                rho2, theta2 = htLinePerpendicular(rho, theta, x_inters, y_inters, img_h, img_w)

                # Compute the image indices of this position which will be the intersection with the stripe
                #   The width of the line will be 2x the angular velocity
                stripe_length = 6*ang_vel
                if stripe_length < stripe_width_factor*config.stripe_width:
                    stripe_length = stripe_width_factor*config.stripe_width
                stripe_indices_motion = getStripeIndices(rho2, theta2, stripe_length, img_h, img_w)

                # Mark only those parts which overlap both lines, which effectively creates a mask for
                #    photometry an centroiding, excluding other influences
                stripe_new = np.zeros_like(stripe)
                stripe_new[stripe_indices_motion] = stripe[stripe_indices_motion]
                stripe = stripe_new


                if debug:

                    # Show the extracted stripe
                    img_stripe = np.zeros_like(stripe)
                    img_stripe[stripe_indices] = 1
                    final_stripe = np.zeros_like(stripe)
                    final_stripe[stripe_indices_motion] = img_stripe[stripe_indices_motion]

                    plt.imshow(final_stripe)
                    plt.show()


            if debug and centroiding:

                print(fr)
                print('mean stdpixel3:', np.mean(img_handle.ff.stdpixel))
                print('mean avepixel3:', np.mean(img_handle.ff.avepixel))
                print('mean frame:', np.mean(fr_img))
                fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True, sharey=True)


                fr_img_noavg = Image.applyDark(fr_img, img_handle.ff.avepixel)
                #fr_img_noavg = fr_img

                # Auto levels
                min_lvl = np.percentile(fr_img_noavg[2:, :], 1)
                max_lvl = np.percentile(fr_img_noavg[2:, :], 99.0)

                # Adjust levels
                fr_img_autolevel = Image.adjustLevels(fr_img_noavg, min_lvl, 1.0, max_lvl)

                ax1.imshow(stripe, cmap='gray')
                ax2.imshow(fr_img_autolevel, cmap='gray')
                plt.show()

                pass


            # Get stripe positions (x, y, frame)
            stripe_positions = stripe.nonzero()
            xs = stripe_positions[1]
            ys = stripe_positions[0]
            zs = np.zeros_like(xs) + fr

            # Add the points to the list
            xs_array.append(xs)
            ys_array.append(ys)
            zs_array.append(zs)


            if debug:
                print('---')
                print(stripe.nonzero())
                print(xs, ys, zs)


        if len(xs_array) > 0:
            
            # Flatten the arrays
            xs_array = np.concatenate(xs_array)
            ys_array = np.concatenate(ys_array)
            zs_array = np.concatenate(zs_array)

        else:
            xs_array = np.array(xs_array)
            ys_array = np.array(ys_array)
            zs_array = np.array(zs_array)


        return xs_array, ys_array, zs_array
Esempio n. 4
0
def extractStars(ff_dir, ff_name, config=None, max_global_intensity=150, border=10, neighborhood_size=10, 
        intensity_threshold=5, flat_struct=None, dark=None, mask=None):
    """ Extracts stars on a given FF bin by searching for local maxima and applying PSF fit for star 
        confirmation.

        Source of one part of the code: 
    http://stackoverflow.com/questions/9111711/get-coordinates-of-local-maxima-in-2d-array-above-certain-value
    
    Arguments:
        ff: [ff bin struct] FF bin file loaded in the FF bin structure
        config: [config object] configuration object (loaded from the .config file)
        max_global_intensity: [int] maximum mean intensity of an image before it is discared as too bright
        border: [int] apply a mask on the detections by removing all that are too close to the given image 
            border (in pixels)
        neighborhood_size: [int] size of the neighbourhood for the maximum search (in pixels)
        intensity_threshold: [float] a threshold for cutting the detections which are too faint (0-255)
        flat_struct: [Flat struct] Structure containing the flat field. None by default.
        dark: [ndarray] Dark frame. None by default.
        mask: [ndarray] Mask image. None by default.

    Return:
        x2, y2, background, intensity, sigma_fitted: [list of ndarrays]
            - x2: X axis coordinates of the star
            - y2: Y axis coordinates of the star
            - background: background intensity
            - intensity: intensity of the star
            - Gaussian stddev of fitted stars
    """

    # This will be returned if there was an error
    error_return = [[], [], [], []]

    # Load parameters from config if given
    if config:
        max_global_intensity = config.max_global_intensity
        border = config.border
        neighborhood_size = config.neighborhood_size
        intensity_threshold = config.intensity_threshold
        

    # Load the FF bin file
    ff = FFfile.read(ff_dir, ff_name)


    # If the FF file could not be read, skip star extraction
    if ff is None:
        return error_return


    # Apply the dark frame
    if dark is not None:
        ff.avepixel = Image.applyDark(ff.avepixel, dark)

    # Apply the flat
    if flat_struct is not None:
        ff.avepixel = Image.applyFlat(ff.avepixel, flat_struct)

    # Mask the FF file
    if mask is not None:
        ff = MaskImage.applyMask(ff, mask, ff_flag=True)


    # Calculate image mean and stddev
    global_mean = np.mean(ff.avepixel)

    # Check if the image is too bright and skip the image
    if global_mean > max_global_intensity:
        return error_return

    data = ff.avepixel.astype(np.float32)


    # Apply a mean filter to the image to reduce noise
    data = ndimage.filters.convolve(data, weights=np.full((2, 2), 1.0/4))

    # Locate local maxima on the image
    data_max = filters.maximum_filter(data, neighborhood_size)
    maxima = (data == data_max)
    data_min = filters.minimum_filter(data, neighborhood_size)
    diff = ((data_max - data_min) > intensity_threshold)
    maxima[diff == 0] = 0

    # Apply a border mask
    border_mask = np.ones_like(maxima)*255
    border_mask[:border,:] = 0
    border_mask[-border:,:] = 0
    border_mask[:,:border] = 0
    border_mask[:,-border:] = 0
    maxima = MaskImage.applyMask(maxima, border_mask, image=True)


    # Find and label the maxima
    labeled, num_objects = ndimage.label(maxima)

    # Skip the image if there are too many maxima to process
    if num_objects > config.max_stars:
        print('Too many candidate stars to process! {:d}/{:d}'.format(num_objects, config.max_stars))
        return error_return

    # Find centres of mass of each labeled objects
    xy = np.array(ndimage.center_of_mass(data, labeled, range(1, num_objects+1)))

    # Remove all detection on the border
    #xy = xy[np.where((xy[:, 1] > border) & (xy[:,1] < ff.ncols - border) & (xy[:,0] > border) & (xy[:,0] < ff.nrows - border))]

    # Unpack star coordinates
    y, x = np.hsplit(xy, 2)

    # # Plot stars before the PSF fit
    # plotStars(ff, x, y)

    # Fit a PSF to each star
    x2, y2, amplitude, intensity, sigma_y_fitted, sigma_x_fitted = fitPSF(ff, global_mean, x, y, config)
    
    # x2, y2, amplitude, intensity = list(x), list(y), [], [] # Skip PSF fit

    # # Plot stars after PSF fit filtering
    # plotStars(ff, x2, y2)
    

    # Compute one dimensional sigma
    sigma_x_fitted = np.array(sigma_x_fitted)
    sigma_y_fitted = np.array(sigma_y_fitted)
    sigma_fitted = np.sqrt(sigma_x_fitted**2 + sigma_y_fitted**2)


    return ff_name, x2, y2, amplitude, intensity, sigma_fitted