Esempio n. 1
0
def binImageCalibration(config, mask, dark, flat_struct):
    """ Bin the calibration images. """

    # Bin the mask
    if mask is not None:
        mask.img = Image.binImage(mask.img, config.detection_binning_factor,
                                  'avg')

    # Bin the dark
    if dark is not None:
        dark = Image.binImage(dark, config.detection_binning_factor, 'avg')

    # Bin the flat
    if flat_struct is not None:
        flat_struct.binFlat(config.detection_binning_factor, 'avg')

    return mask, dark, flat_struct
Esempio n. 2
0
def plotStars(ff, x2, y2):
    """ Plots detected stars on the input image.
    """

    # Plot image with adjusted levels to better see stars
    plt.imshow(Image.adjustLevels(ff.avepixel, 0, 1.3, 255), cmap='gray')

    # Plot stars
    for star in zip(list(y2), list(x2)):
        y, x = star
        c = plt.Circle((x, y), 5, fill=False, color='r')
        plt.gca().add_patch(c)

    plt.show()

    plt.clf()
    plt.close()
Esempio n. 3
0
def plotStars(ff, x2, y2):
    """ Plots detected stars on the input image.
    """

    # Plot image with adjusted levels to better see stars
    plt.imshow(Image.adjustLevels(ff.avepixel, 0, 1.3, 255), cmap='gray')

    # Plot stars
    for star in zip(list(y2), list(x2)):
        y, x = star
        c = plt.Circle((x, y), 5, fill=False, color='r')
        plt.gca().add_patch(c)

    plt.show()

    plt.clf()
    plt.close()
Esempio n. 4
0
def loadImageCalibration(dir_path, config, dtype=None, byteswap=False):
    """ Load the mask, dark and flat. 
    
    Arguments:
        dir_path: [str] Path to the directory with calibration.
        config: [ConfigStruct]

    Keyword arguments:
        dtype: [object] Numpy array dtype for the image. None by default, if which case it will be determined
            from the input image.
        byteswap: [bool] If the dark and flat should be byteswapped. False by default, and should be True for
            UWO PNGs.

    Return:
        mask, dark, flat_struct: [tuple of ndarrays]
    """

    mask_path = None
    mask = None

    # Try loading the mask
    if os.path.exists(os.path.join(dir_path, config.mask_file)):
        mask_path = os.path.join(dir_path, config.mask_file)

    # Try loading the default mask
    elif os.path.exists(config.mask_file):
        mask_path = os.path.abspath(config.mask_file)

    # Load the mask if given
    if mask_path:
        mask = MaskImage.loadMask(mask_path)

    if mask is not None:
        print('Loaded mask:', mask_path)
        log.info('Loaded mask: {:s}'.format(mask_path))

    # Try loading the dark frame
    dark = None
    if config.use_dark:

        dark_path = None

        # Check if dark is in the data directory
        if os.path.exists(os.path.join(dir_path, config.dark_file)):
            dark_path = os.path.join(dir_path, config.dark_file)

        # Try loading the default dark
        elif os.path.exists(config.dark_file):
            dark_path = os.path.abspath(config.dark_file)

        if dark_path is not None:

            # Load the dark
            dark = Image.loadDark(*os.path.split(dark_path),
                                  dtype=dtype,
                                  byteswap=byteswap)

        if dark is not None:
            print('Loaded dark:', dark_path)
            log.info('Loaded dark: {:s}'.format(dark_path))

    # Try loading a flat field image
    flat_struct = None
    if config.use_flat:

        flat_path = None

        # Check if there is flat in the data directory
        if os.path.exists(os.path.join(dir_path, config.flat_file)):
            flat_path = os.path.join(dir_path, config.flat_file)

        # Try loading the default flat
        elif os.path.exists(config.flat_file):
            flat_path = os.path.abspath(config.flat_file)

        if flat_path is not None:

            # Load the flat
            flat_struct = Image.loadFlat(*os.path.split(flat_path),
                                         dtype=dtype,
                                         byteswap=byteswap)

        if flat_struct is not None:
            print('Loaded flat:', flat_path)
            log.info('Loaded flat: {:s}'.format(flat_path))

    return mask, dark, flat_struct
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
def loadImageCalibration(dir_path, config, dtype=None, byteswap=False):
    """ Load the mask, dark and flat. 
    
    Arguments:
        dir_path: [str] Path to the directory with calibration.
        config: [ConfigStruct]

    Keyword arguments:
        dtype: [object] Numpy array dtype for the image. None by default, if which case it will be determined
            from the input image.
        byteswap: [bool] If the dark and flat should be byteswapped. False by default, and should be True for
            UWO PNGs.

    Return:
        mask, dark, flat_struct: [tuple of ndarrays]
    """

    mask_path = None
    mask = None

    # Try loading the mask
    if os.path.exists(os.path.join(dir_path, config.mask_file)):
        mask_path = os.path.join(dir_path, config.mask_file)

    # Try loading the default mask
    elif os.path.exists(config.mask_file):
        mask_path = os.path.abspath(config.mask_file)

    # Load the mask if given
    if mask_path:
        mask = MaskImage.loadMask(mask_path)

    if mask is not None:
        print('Loaded mask:', mask_path)
        log.info('Loaded mask: {:s}'.format(mask_path))




    # Try loading the dark frame
    dark = None
    if config.use_dark:

        dark_path = None

        # Check if dark is in the data directory
        if os.path.exists(os.path.join(dir_path, config.dark_file)):
            dark_path = os.path.join(dir_path, config.dark_file)

        # Try loading the default dark
        elif os.path.exists(config.dark_file):
            dark_path = os.path.abspath(config.dark_file)

        if dark_path is not None:

            # Load the dark
            dark = Image.loadDark(*os.path.split(dark_path), dtype=dtype, byteswap=byteswap)

        if dark is not None:
            print('Loaded dark:', dark_path)
            log.info('Loaded dark: {:s}'.format(dark_path))



    # Try loading a flat field image
    flat_struct = None
    if config.use_flat:

        flat_path = None
        
        # Check if there is flat in the data directory
        if os.path.exists(os.path.join(dir_path, config.flat_file)):
            flat_path = os.path.join(dir_path, config.flat_file)
            
        # Try loading the default flat
        elif os.path.exists(config.flat_file):
            flat_path = os.path.abspath(config.flat_file)

        if flat_path is not None:
            
            # Load the flat
            flat_struct = Image.loadFlat(*os.path.split(flat_path), dtype=dtype, byteswap=byteswap)


        if flat_struct is not None:
            print('Loaded flat:', flat_path)
            log.info('Loaded flat: {:s}'.format(flat_path))



    return mask, dark, flat_struct
Esempio n. 7
0
def detectMeteors(ff_directory, ff_name, config, flat_struct=None):
    """ Detect meteors on the given FF bin image. Here are the steps in the detection:
            - input image (FF bin format file) is thresholded (converted to black and white)
            - several morphological operations are applied to clean the image
            - image is then broken into several image "windows" (these "windows" are reconstructed from the input FF file, given
              an input frame range (e.g. 64-128) which helps reduce the noise further)
            - on each "window" the Kernel-based Hough transform is performed to find any lines on the image
            - similar lines are joined
            - stripe around the lines is extracted
            - 3D line finding (third dimension is time) is applied to check if the line propagates in time
            - centroiding is performed, which calculates the position and intensity of meteor on each frame
    
    Arguments:
        ff_directory: [string] an absolute path to the input FF bin file
        ff_name: [string] file name of the FF bin file on which to run the detection on
        config: [config object] configuration object (loaded from the .config file)

    Keyword arguments:
        flat_struct: [Flat struct] Structure containing the flat field. None by default.
    
    Return:
        meteor_detections: [list] a list of detected meteors, with these elements:
            - rho: [float] meteor line distance from image center (polar coordinates, in pixels)
            - theta: [float] meteor line angle from image center (polar coordinates, in degrees)
            - centroids: [list] [frame, X, Y, level] list of meteor points
    """

    t1 = time()
    t_all = time()

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

    # Load the mask file
    mask = MaskImage.loadMask(config.mask_file)

    # Mask the FF file
    ff = MaskImage.applyMask(ff, mask, ff_flag=True)

    # Apply the flat to maxpixel and avepixel
    if flat_struct is not None:

        ff.maxpixel = Image.applyFlat(ff.maxpixel, flat_struct)
        ff.avepixel = Image.applyFlat(ff.avepixel, flat_struct)

    # At the end, a check that the detection has a surface brightness above the background will be performed.
    # The assumption here is that the peak of the meteor should have the intensity which is at least
    # that of a patch of 4x4 pixels that are of the mean background brightness
    min_patch_intensity = 4 * 4 * (np.mean(ff.maxpixel - ff.avepixel) +
                                   config.k1_det * np.mean(ff.stdpixel) +
                                   config.j1)

    # # Show the maxpixel image
    # show2(ff_name+' maxpixel', ff.maxpixel)

    # Get lines on the image
    line_list = getLines(ff, config.k1_det, config.j1_det, config.time_slide,
                         config.time_window_size, config.max_lines_det,
                         config.max_white_ratio, config.kht_lib_path)

    logDebug('List of lines:', line_list)

    # Init meteor list
    meteor_detections = []

    # Only if there are some lines in the image
    if len(line_list):

        # Join similar lines
        line_list = mergeLines(line_list, config.line_min_dist, ff.ncols,
                               ff.nrows)

        logDebug('Time for finding lines:', time() - t1)

        logDebug('Number of KHT lines: ', len(line_list))
        logDebug(line_list)

        # Plot lines
        # plotLines(ff, line_list)

        # Threshold the image
        img_thres = thresholdImg(ff, config.k1_det, config.j1_det)

        filtered_lines = []

        # Analyze stripes of each line
        for line in line_list:
            rho, theta, frame_min, frame_max = line

            logDebug('rho, theta, frame_min, frame_max')
            logDebug(rho, theta, frame_min, frame_max)

            # Bounded the thresholded image by min and max frames
            img = selectFrames(np.copy(img_thres), ff, frame_min, frame_max)

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

            # Get indices of stripe pixels around the line
            stripe_indices = getStripeIndices(rho, theta, config.stripe_width,
                                              img.shape[0], img.shape[1])

            # Extract the stripe from the thresholded image
            stripe = np.zeros((ff.nrows, ff.ncols), np.uint8)
            stripe[stripe_indices] = img[stripe_indices]

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

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

            # Get stripe positions
            stripe_positions = stripe.nonzero()
            xs = stripe_positions[1]
            ys = stripe_positions[0]
            zs = ff.maxframe[stripe_positions]

            # Limit the number of points to search if too large
            if len(zs) > config.max_points_det:

                # Extract weights of each point
                maxpix_elements = ff.maxpixel[ys, xs].astype(np.float64)
                weights = maxpix_elements / np.sum(maxpix_elements)

                # Random sample the point, sampling is weighted by pixel intensity
                indices = np.random.choice(len(zs),
                                           config.max_points_det,
                                           replace=False,
                                           p=weights)
                ys = ys[indices]
                xs = xs[indices]
                zs = zs[indices]

            # Make an array to feed into the gropuing algorithm
            stripe_points = np.vstack((xs, ys, zs))
            stripe_points = np.swapaxes(stripe_points, 0, 1)

            # Sort stripe points by frame
            stripe_points = stripe_points[stripe_points[:, 2].argsort()]

            t1 = time()

            logDebug('finding lines...')

            # Find a single line in the point cloud
            detected_line = find3DLines(stripe_points,
                                        time(),
                                        config,
                                        fireball_detection=False)

            logDebug('time for GROUPING: ', time() - t1)

            # Extract the first and only line if any
            if detected_line:
                detected_line = detected_line[0]

                # logDebug(detected_line)

                # Show 3D cloud
                # show3DCloud(ff, stripe, detected_line, stripe_points, config)

                # Add the line to the results list
                filtered_lines.append(detected_line)

        # Merge similar lines in 3D
        filtered_lines = merge3DLines(filtered_lines, config.vect_angle_thresh)

        logDebug('after filtering:')
        logDebug(filtered_lines)

        for detected_line in filtered_lines:

            # Get frame range
            frame_min = detected_line[4]
            frame_max = detected_line[5]

            # Check if the line covers a minimum frame range
            if (abs(frame_max - frame_min) + 1 <
                    config.line_minimum_frame_range_det):
                continue

            # Extand the frame range for several frames, just to be sure to catch all parts of a meteor
            frame_min -= config.frame_extension
            frame_max += config.frame_extension

            # Cap values to 0-255
            frame_min = max(frame_min, 0)
            frame_max = min(frame_max, 255)

            logDebug(detected_line)

            # Get coordinates of 2 points that describe the line
            x1, y1, z1 = detected_line[0]
            x2, y2, z2 = detected_line[1]

            # Convert Cartesian line coordinates to polar
            rho, theta = getPolarLine(x1, y1, x2, y2, ff.nrows, ff.ncols)

            # Convert Cartesian line coordinate to CAMS compatible polar coordinates (flipped Y axis)
            rho_cams, theta_cams = getPolarLine(x1, ff.nrows - y1, x2,
                                                ff.nrows - y2, ff.nrows,
                                                ff.ncols)

            logDebug('converted rho, theta')
            logDebug(rho, theta)

            # Bounded the thresholded image by min and max frames
            img = selectFrames(np.copy(img_thres), ff, frame_min, frame_max)

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

            # Get indices of stripe pixels around the line
            stripe_indices = getStripeIndices(rho, theta,
                                              int(config.stripe_width * 1.5),
                                              img.shape[0], img.shape[1])

            # Extract the stripe from the thresholded image
            stripe = np.zeros((ff.nrows, ff.ncols), np.uint8)
            stripe[stripe_indices] = img[stripe_indices]

            # Show detected line
            # show('detected line: '+str(frame_min)+'-'+str(frame_max), stripe)

            # Get stripe positions
            stripe_positions = stripe.nonzero()
            xs = stripe_positions[1]
            ys = stripe_positions[0]
            zs = ff.maxframe[stripe_positions]

            # Make an array to feed into the centroiding algorithm
            stripe_points = np.vstack((xs, ys, zs))
            stripe_points = np.swapaxes(stripe_points, 0, 1)

            # Sort stripe points by frame
            stripe_points = stripe_points[stripe_points[:, 2].argsort()]

            # Show 3D cloud
            # show3DCloud(ff, stripe, detected_line, stripe_points, config)

            # Get points of the given line
            line_points = getAllPoints(stripe_points,
                                       x1,
                                       y1,
                                       z1,
                                       x2,
                                       y2,
                                       z2,
                                       config,
                                       fireball_detection=False)

            # Skip if no points were returned
            if not line_points.any():
                continue

            # Skip if the points cover too small a frame range
            if abs(np.max(line_points[:, 2]) - np.min(line_points[:, 2])
                   ) + 1 < config.line_minimum_frame_range_det:
                continue

            # Calculate centroids
            centroids = []

            for i in range(frame_min, frame_max + 1):

                # Select pixel indicies belonging to a given frame
                frame_pixels_inds = np.where(line_points[:, 2] == i)

                # Get pixel positions in a given frame (pixels belonging to a found line)
                frame_pixels = line_points[frame_pixels_inds].astype(np.int64)

                # Get pixel positions in a given frame (pixels belonging to the whole stripe)
                frame_pixels_stripe = stripe_points[np.where(
                    stripe_points[:, 2] == i)].astype(np.int64)

                # Skip if there are no pixels in the frame
                if not len(frame_pixels):
                    continue

                # Calculate weights for centroiding
                max_avg_corrected = ff.maxpixel - ff.avepixel
                flattened_weights = (max_avg_corrected).astype(
                    np.float32) / ff.stdpixel

                # Calculate centroids by half-frame
                for half_frame in range(2):

                    # Apply deinterlacing if it is present in the video
                    if config.deinterlace_order >= 0:

                        # Deinterlace by fields (line lixels)
                        half_frame_pixels = frame_pixels[
                            frame_pixels[:, 1] %
                            2 == (config.deinterlace_order + half_frame) % 2]

                        # Deinterlace by fields (stripe pixels)
                        half_frame_pixels_stripe = frame_pixels_stripe[
                            frame_pixels_stripe[:, 1] %
                            2 == (config.deinterlace_order + half_frame) % 2]

                        # Skip if there are no pixels in the half-frame
                        if not len(half_frame_pixels):
                            continue

                        # Calculate half-frame value
                        frame_no = i + half_frame * 0.5

                    # No deinterlacing
                    else:

                        # Skip the second half frame
                        if half_frame == 1:
                            continue

                        half_frame_pixels = frame_pixels
                        half_frame_pixels_stripe = frame_pixels_stripe
                        frame_no = i

                    # Get maxpixel-avepixel values of given pixel indices (this will be used as weights)
                    max_weights = flattened_weights[half_frame_pixels[:, 1],
                                                    half_frame_pixels[:, 0]]

                    # Calculate weighted centroids
                    x_weighted = half_frame_pixels[:, 0] * np.transpose(
                        max_weights)
                    x_centroid = np.sum(x_weighted) / float(
                        np.sum(max_weights))

                    y_weighted = half_frame_pixels[:, 1] * np.transpose(
                        max_weights)
                    y_centroid = np.sum(y_weighted) / float(
                        np.sum(max_weights))

                    # Calculate intensity as the sum of threshold passer pixels on the stripe
                    #intensity_values = max_avg_corrected[half_frame_pixels[:,1], half_frame_pixels[:,0]]
                    intensity_values = max_avg_corrected[
                        half_frame_pixels_stripe[:, 1],
                        half_frame_pixels_stripe[:, 0]]
                    intensity = np.sum(intensity_values)

                    logDebug("centroid: ", frame_no, x_centroid, y_centroid,
                             intensity)

                    centroids.append(
                        [frame_no, x_centroid, y_centroid, intensity])

            # Filter centroids
            centroids = filterCentroids(centroids,
                                        config.centroids_max_deviation,
                                        config.centroids_max_distance)

            # Convert to numpy array for easy slicing
            centroids = np.array(centroids)

            # Reject the solution if there are too few centroids
            if len(centroids) < config.line_minimum_frame_range_det:
                continue

            # Check that the detection has a surface brightness above the background
            # The assumption here is that the peak of the meteor should have the intensity which is at least
            # that of a patch of 4x4 pixels that are of the mean background brightness
            if np.max(centroids[:, 3]) < min_patch_intensity:
                continue

            # Check the detection if it has the proper angular velocity
            if not checkAngularVelocity(centroids, config):
                continue

            # Append the result to the meteor detections
            meteor_detections.append([rho_cams, theta_cams, centroids])

            logDebug('time for processing:', time() - t_all)

            # # Plot centroids to image
            # fig, (ax1, ax2) = plt.subplots(nrows=2)

            # ax1.imshow(ff.maxpixel - ff.avepixel, cmap='gray')
            # ax1.scatter(centroids[:,1], centroids[:,2], s=5, c='r', edgecolors='none')

            # # Plot lightcurve
            # ax2.plot(centroids[:,0], centroids[:,3])

            # # # Plot relative angular velocity
            # # ang_vels = []
            # # fr_prev, x_prev, y_prev, _ = centroids[0]
            # # for fr, x, y, _ in centroids[1:]:
            # #     dx = x - x_prev
            # #     dy = y - y_prev
            # #     dfr = fr - fr_prev

            # #     ddist = np.sqrt(dx**2 + dy**2)
            # #     dt = dfr/config.fps

            # #     ang_vels.append(ddist/dt)

            # #     x_prev = x
            # #     y_prev = y
            # #     fr_prev = fr

            # # ax2.plot(ang_vels)

            # plt.show()

    return meteor_detections
Esempio n. 8
0
def fitPSF(ff, avepixel_mean, x2, y2, config):
    """ Fit a 2D Gaussian to the star candidate cutout to check if it's a star.
    
    Arguments:
        ff: [ff bin struct] FF bin file loaded in the FF bin structure
        avepixel_mean: [float] mean of the avepixel image
        x2: [list] a list of estimated star position (X axis)
        xy: [list] a list of estimated star position (Y axis)
        config: [config object] configuration object (loaded from the .config file)
    """

    # Load parameters form config if present
    if config is not None:
        # segment_radius: [int] radius (in pixels) of image segment around the detected star on which to 
        #     perform the fit
        # roundness_threshold: [float] minimum ratio of 2D Gaussian sigma X and sigma Y to be taken as a stars
        #     (hot pixels are narrow, while stars are round)
        # max_feature_ratio: [float] maximum ratio between 2 sigma of the star and the image segment area
        segment_radius = config.segment_radius
        roundness_threshold = config.roundness_threshold
        max_feature_ratio = config.max_feature_ratio


    x_fitted = []
    y_fitted = []
    amplitude_fitted = []
    intensity_fitted = []
    sigma_y_fitted = []
    sigma_x_fitted = []

    # Set the initial guess
    initial_guess = (30.0, segment_radius, segment_radius, 1.0, 1.0, 0.0, avepixel_mean)
    
    # Go through all stars
    for star in zip(list(y2), list(x2)):

        y, x = star

        y_min = y - segment_radius
        y_max = y + segment_radius
        x_min = x - segment_radius
        x_max = x + segment_radius

        if y_min < 0:
            y_min = 0
        if y_max > ff.nrows:
            y_max = ff.nrows
        if x_min < 0:
            x_min = 0
        if x_max > ff.ncols:
            x_max = ff.ncols


        x_min = int(x_min)
        x_max = int(x_max)
        y_min = int(y_min)
        y_max = int(y_max)

        # Extract an image segment around each star
        star_seg = ff.avepixel[y_min:y_max, x_min:x_max]

        # Create x and y indices
        y_ind, x_ind = np.indices(star_seg.shape)

        # Estimate saturation level from image type
        saturation = 2**(8*star_seg.itemsize) - 1

        # Fit a PSF to the star
        try:
            # Fit the 2D Gaussian with the limited number of iterations - this reduces the processing time
            # and most of the bad star candidates take more iterations to fit
            popt, pcov = opt.curve_fit(twoDGaussian, (y_ind, x_ind, saturation), star_seg.ravel(), \
                p0=initial_guess, maxfev=200)
            # print(popt)
        except RuntimeError:
            # print('Fitting failed!')

            # Skip stars that can't be fitted in 200 iterations
            continue

        # Unpack fitted gaussian parameters
        amplitude, yo, xo, sigma_y, sigma_x, theta, offset = popt

        # Filter hot pixels by looking at the ratio between x and y sigmas (HPs are very narrow)
        if min(sigma_y/sigma_x, sigma_x/sigma_y) < roundness_threshold:
            # Skip if it is a hot pixel
            continue

        # Reject the star candidate if it is too large 
        if (4*sigma_x*sigma_y / segment_radius**2 > max_feature_ratio):
            continue


        ### If the fitting was successfull, compute the star intensity

        # Crop the star segment to take 3 sigma portion around the star
        crop_y_min = int(yo - 3*sigma_y) + 1
        if crop_y_min < 0: crop_y_min = 0
        
        crop_y_max = int(yo + 3*sigma_y) + 1
        if crop_y_max >= star_seg.shape[0]: crop_y_max = star_seg.shape[0] - 1

        crop_x_min = int(xo - 3*sigma_x) + 1
        if crop_x_min < 0: crop_x_min = 0

        crop_x_max = int(xo + 3*sigma_x) + 1
        if crop_x_max >= star_seg.shape[1]: crop_x_max = star_seg.shape[1] - 1

        # If the segment is too small, set a fixed size
        if (y_max - y_min) < 3:
            crop_y_min = int(yo - 2)
            crop_y_max = int(yo + 2)

        if (x_max - x_min) < 3:
            crop_x_min = int(xo - 2)
            crop_x_max = int(xo + 2)


        star_seg_crop = star_seg[crop_y_min:crop_y_max, crop_x_min:crop_x_max]

        # Skip the star if the shape is too small
        if (star_seg_crop.shape[0] == 0) or (star_seg_crop.shape[1] == 0):
            continue

        # Gamma correct the star segment
        star_seg_crop = Image.gammaCorrection(star_seg_crop.astype(np.float32), config.gamma)

        # Correct the background for gamma
        bg_corrected = Image.gammaCorrection(offset, config.gamma)

        # Subtract the background from the star segment and compute the total intensity
        intensity = np.sum(star_seg_crop - bg_corrected)

        # Skip stars with zero intensity
        if intensity <= 0:
            continue

        # print(intensity)
        # plt.imshow(star_seg_crop - bg_corrected, cmap='gray', vmin=0, vmax=255)
        # plt.show()


        ###

        # Calculate the intensity (as a volume under the 2D Gaussian) (OLD, before gamma correction)
        # intensity = 2*np.pi*amplitude*sigma_x*sigma_y



        # # Skip if the star intensity is below background level
        # if intensity < offset:
        #     continue

        # Add stars to the final list
        x_fitted.append(x_min + xo)
        y_fitted.append(y_min + yo)
        amplitude_fitted.append(amplitude)
        intensity_fitted.append(intensity)
        sigma_y_fitted.append(sigma_y)
        sigma_x_fitted.append(sigma_x)

        # # Plot fitted stars
        # data_fitted = twoDGaussian((y_ind, x_ind), *popt) - offset

        # fig, ax = plt.subplots(1, 1)
        # ax.hold(True)
        # plt.title('Center Y: '+str(y_min[0])+', X:'+str(x_min[0]))
        # ax.imshow(star_seg.reshape(segment_radius*2, segment_radius*2), cmap=plt.cm.inferno, origin='bottom',
        #     extent=(x_ind.min(), x_ind.max(), y_ind.min(), y_ind.max()))
        # # ax.imshow(data_fitted.reshape(segment_radius*2, segment_radius*2), cmap=plt.cm.jet, origin='bottom')
        # ax.contour(x_ind, y_ind, data_fitted.reshape(segment_radius*2, segment_radius*2), 8, colors='w')

        # plt.show()
        # plt.clf()
        # plt.close()

    return x_fitted, y_fitted, amplitude_fitted, intensity_fitted, sigma_y_fitted, sigma_x_fitted
Esempio n. 9
0
def runCapture(config,
               duration=None,
               video_file=None,
               nodetect=False,
               detect_end=False,
               upload_manager=None):
    """ Run capture and compression for the given time.given

    Arguments:
        config: [config object] Configuration read from the .config file

    Keyword arguments:
        duration: [float] Time in seconds to capture. None by default.
        video_file: [str] Path to the video file, if it was given as the video source. None by default.
        nodetect: [bool] If True, detection will not be performed. False by defualt.
        detect_end: [bool] If True, detection will be performed at the end of the night, when capture 
            finishes. False by default.
        upload_manager: [UploadManager object] A handle to the UploadManager, which handles uploading files to
            the central server. None by default.

    """

    global STOP_CAPTURE

    # Create a directory for captured files
    night_data_dir_name = str(
        config.stationID) + '_' + datetime.datetime.utcnow().strftime(
            '%Y%m%d_%H%M%S_%f')

    # Full path to the data directory
    night_data_dir = os.path.join(os.path.abspath(config.data_dir),
                                  config.captured_dir, night_data_dir_name)

    # Make a directory for the night
    mkdirP(night_data_dir)

    log.info('Data directory: ' + night_data_dir)

    # Load the default flat field image if it is available
    flat_struct = None

    if config.use_flat:

        # Check if the flat exists
        if os.path.exists(os.path.join(os.getcwd(), config.flat_file)):
            flat_struct = Image.loadFlat(os.getcwd(), config.flat_file)

            log.info('Loaded flat field image: ' +
                     os.path.join(os.getcwd(), config.flat_file))

    # Get the platepar file
    platepar, platepar_path, platepar_fmt = getPlatepar(config)

    log.info('Initializing frame buffers...')
    ### For some reason, the RPi 3 does not like memory chunks which size is the multipier of its L2
    ### cache size (512 kB). When such a memory chunk is provided, the compression becomes 10x slower
    ### then usual. We are applying a dirty fix here where we just add an extra image row and column
    ### if such a memory chunk will be created. The compression is performed, and the image is cropped
    ### back to its original dimensions.
    array_pad = 0

    # Check if the image dimensions are divisible by RPi3 L2 cache size and add padding
    if (256 * config.width * config.height) % (512 * 1024) == 0:
        array_pad = 1

    # Init arrays for parallel compression on 2 cores
    sharedArrayBase = multiprocessing.Array(
        ctypes.c_uint8,
        256 * (config.width + array_pad) * (config.height + array_pad))
    sharedArray = np.ctypeslib.as_array(sharedArrayBase.get_obj())
    sharedArray = sharedArray.reshape(256, (config.height + array_pad),
                                      (config.width + array_pad))
    startTime = multiprocessing.Value('d', 0.0)

    sharedArrayBase2 = multiprocessing.Array(
        ctypes.c_uint8,
        256 * (config.width + array_pad) * (config.height + array_pad))
    sharedArray2 = np.ctypeslib.as_array(sharedArrayBase2.get_obj())
    sharedArray2 = sharedArray2.reshape(256, (config.height + array_pad),
                                        (config.width + array_pad))
    startTime2 = multiprocessing.Value('d', 0.0)

    log.info('Initializing frame buffers done!')

    # Check if the detection should be performed or not
    if nodetect:
        detector = None

    else:

        if detect_end:

            # Delay detection until the end of the night
            delay_detection = duration

        else:
            # Delay the detection for 2 minutes after capture start
            delay_detection = 120

        # Initialize the detector
        detector = QueuedPool(detectStarsAndMeteors,
                              cores=1,
                              log=log,
                              delay_start=delay_detection)
        detector.startPool()

    # Initialize buffered capture
    bc = BufferedCapture(sharedArray,
                         startTime,
                         sharedArray2,
                         startTime2,
                         config,
                         video_file=video_file)

    # Initialize the live image viewer
    live_view = LiveViewer(window_name='Maxpixel')

    # Initialize compression
    compressor = Compressor(night_data_dir,
                            sharedArray,
                            startTime,
                            sharedArray2,
                            startTime2,
                            config,
                            detector=detector,
                            live_view=live_view,
                            flat_struct=flat_struct)

    # Start buffered capture
    bc.startCapture()

    # Start the compression
    compressor.start()

    # Capture until Ctrl+C is pressed
    wait(duration)

    # If capture was manually stopped, end capture
    if STOP_CAPTURE:
        log.info('Ending capture...')

    # Stop the capture
    log.debug('Stopping capture...')
    bc.stopCapture()
    log.debug('Capture stopped')

    dropped_frames = bc.dropped_frames
    log.info('Total number of dropped frames: ' + str(dropped_frames))

    # Stop the compressor
    log.debug('Stopping compression...')
    detector, live_view = compressor.stop()
    log.debug('Compression stopped')

    # Stop the live viewer
    log.debug('Stopping live viewer...')
    live_view.stop()
    del live_view
    log.debug('Live view stopped')

    # Init data lists
    star_list = []
    meteor_list = []
    ff_detected = []

    # If detection should be performed
    if not nodetect:

        log.info('Finishing up the detection, ' +
                 str(detector.input_queue.qsize()) + ' files to process...')

        # Reset the Ctrl+C to KeyboardInterrupt
        resetSIGINT()

        try:

            # If there are some more files to process, process them on more cores
            if detector.input_queue.qsize() > 0:

                # Let the detector use all cores, but leave 1 free
                available_cores = multiprocessing.cpu_count() - 1

                if available_cores > 1:

                    log.info('Running the detection on {:d} cores...'.format(
                        available_cores))

                    # Start the detector
                    detector.updateCoreNumber(cores=available_cores)

            log.info('Waiting for the detection to finish...')

            # Wait for the detector to finish and close it
            detector.closePool()

            log.info('Detection finished!')

        except KeyboardInterrupt:

            log.info('Ctrl + C pressed, exiting...')

            if upload_manager is not None:

                # Stop the upload manager
                if upload_manager.is_alive():
                    log.debug('Closing upload manager...')
                    upload_manager.stop()
                    del upload_manager

            # Terminate the detector
            if detector is not None:
                del detector

            sys.exit()

        # Set the Ctrl+C back to 'soft' program kill
        setSIGINT()

        ### SAVE DETECTIONS TO FILE

        log.info('Collecting results...')

        # Get the detection results from the queue
        detection_results = detector.getResults()

        # Remove all 'None' results, which were errors
        detection_results = [
            res for res in detection_results if res is not None
        ]

        # Count the number of detected meteors
        meteors_num = 0
        for _, _, meteor_data in detection_results:
            for meteor in meteor_data:
                meteors_num += 1

        log.info('TOTAL: ' + str(meteors_num) + ' detected meteors.')

        # Save the detections to a file
        for ff_name, star_data, meteor_data in detection_results:

            x2, y2, background, intensity = star_data

            # Skip if no stars were found
            if not x2:
                continue

            # Construct the table of the star parameters
            star_data = zip(x2, y2, background, intensity)

            # Add star info to the star list
            star_list.append([ff_name, star_data])

            # Handle the detected meteors
            meteor_No = 1
            for meteor in meteor_data:

                rho, theta, centroids = meteor

                # Append to the results list
                meteor_list.append([ff_name, meteor_No, rho, theta, centroids])
                meteor_No += 1

            # Add the FF file to the archive list if a meteor was detected on it
            if meteor_data:
                ff_detected.append(ff_name)

        # Generate the name for the CALSTARS file
        calstars_name = 'CALSTARS_' + "{:s}".format(str(config.stationID)) + '_' \
            + os.path.basename(night_data_dir) + '.txt'

        # Write detected stars to the CALSTARS file
        CALSTARS.writeCALSTARS(star_list, night_data_dir, calstars_name, config.stationID, config.height, \
            config.width)

        # Generate FTPdetectinfo file name
        ftpdetectinfo_name = 'FTPdetectinfo_' + os.path.basename(
            night_data_dir) + '.txt'

        # Write FTPdetectinfo file
        FTPdetectinfo.writeFTPdetectinfo(meteor_list, night_data_dir, ftpdetectinfo_name, night_data_dir, \
            config.stationID, config.fps)

        # Get the platepar file
        platepar, platepar_path, platepar_fmt = getPlatepar(config)

        # Run calibration check and auto astrometry refinement
        if platepar is not None:

            # Read in the CALSTARS file
            calstars_list = CALSTARS.readCALSTARS(night_data_dir,
                                                  calstars_name)

            # Run astrometry check and refinement
            platepar, fit_status = autoCheckFit(config, platepar,
                                                calstars_list)

            # If the fit was sucessful, apply the astrometry to detected meteors
            if fit_status:

                log.info('Astrometric calibration SUCCESSFUL!')

                # Save the refined platepar to the night directory and as default
                platepar.write(os.path.join(night_data_dir,
                                            config.platepar_name),
                               fmt=platepar_fmt)
                platepar.write(platepar_path, fmt=platepar_fmt)

            else:
                log.info(
                    'Astrometric calibration FAILED!, Using old platepar for calibration...'
                )

            # Calculate astrometry for meteor detections
            applyAstrometryFTPdetectinfo(night_data_dir, ftpdetectinfo_name,
                                         platepar_path)

    log.info('Plotting field sums...')

    # Plot field sums to a graph
    plotFieldsums(night_data_dir, config)

    # Archive all fieldsums to one archive
    archiveFieldsums(night_data_dir)

    # List for any extra files which will be copied to the night archive directory. Full paths have to be
    #   given
    extra_files = []

    log.info('Making a flat...')

    # Make a new flat field
    flat_img = makeFlat(night_data_dir, config)

    # If making flat was sucessfull, save it
    if flat_img is not None:

        # Save the flat in the root directory, to keep the operational flat updated
        scipy.misc.imsave(config.flat_file, flat_img)
        flat_path = os.path.join(os.getcwd(), config.flat_file)
        log.info('Flat saved to: ' + flat_path)

        # Copy the flat to the night's directory as well
        extra_files.append(flat_path)

    else:
        log.info('Making flat image FAILED!')

    ### Add extra files to archive

    # Add the platepar to the archive if it exists
    if os.path.exists(platepar_path):
        extra_files.append(platepar_path)

    # Add the config file to the archive too
    extra_files.append(os.path.join(os.getcwd(), '.config'))

    ### ###

    night_archive_dir = os.path.join(os.path.abspath(config.data_dir),
                                     config.archived_dir, night_data_dir_name)

    log.info('Archiving detections to ' + night_archive_dir)

    # Archive the detections
    archive_name = archiveDetections(night_data_dir, night_archive_dir, ff_detected, config, \
        extra_files=extra_files)

    # Put the archive up for upload
    if upload_manager is not None:
        log.info('Adding file on upload list: ' + archive_name)
        upload_manager.addFiles([archive_name])

    # If capture was manually stopped, end program
    if STOP_CAPTURE:

        log.info('Ending program')

        # Stop the upload manager
        if upload_manager is not None:
            if upload_manager.is_alive():
                upload_manager.stop()
                log.info('Closing upload manager...')

        sys.exit()
Esempio n. 10
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
Esempio n. 11
0
def fitPSF(ff, avepixel_mean, x2, y2, config):
    """ Fit a 2D Gaussian to the star candidate cutout to check if it's a star.
    
    Arguments:
        ff: [ff bin struct] FF bin file loaded in the FF bin structure
        avepixel_mean: [float] mean of the avepixel image
        x2: [list] a list of estimated star position (X axis)
        xy: [list] a list of estimated star position (Y axis)
        config: [config object] configuration object (loaded from the .config file)
    """

    # Load parameters form config if present
    if config is not None:
        # segment_radius: [int] radius (in pixels) of image segment around the detected star on which to
        #     perform the fit
        # roundness_threshold: [float] minimum ratio of 2D Gaussian sigma X and sigma Y to be taken as a stars
        #     (hot pixels are narrow, while stars are round)
        # max_feature_ratio: [float] maximum ratio between 2 sigma of the star and the image segment area
        segment_radius = config.segment_radius
        roundness_threshold = config.roundness_threshold
        max_feature_ratio = config.max_feature_ratio

    x_fitted = []
    y_fitted = []
    amplitude_fitted = []
    intensity_fitted = []
    sigma_y_fitted = []
    sigma_x_fitted = []

    # Set the initial guess
    initial_guess = (30.0, segment_radius, segment_radius, 1.0, 1.0, 0.0,
                     avepixel_mean)

    # Go through all stars
    for star in zip(list(y2), list(x2)):

        y, x = star

        y_min = y - segment_radius
        y_max = y + segment_radius
        x_min = x - segment_radius
        x_max = x + segment_radius

        if y_min < 0:
            y_min = np.array([0])
        if y_max > ff.nrows:
            y_max = np.array([ff.nrows])
        if x_min < 0:
            x_min = np.array([0])
        if x_max > ff.ncols:
            x_max = np.array([ff.ncols])

        # Check if any of these values is NaN and skip the star
        if np.any(np.isnan([x_min, x_max, y_min, y_max])):
            continue

        x_min = int(x_min)
        x_max = int(x_max)
        y_min = int(y_min)
        y_max = int(y_max)

        # Extract an image segment around each star
        star_seg = ff.avepixel[y_min:y_max, x_min:x_max]

        # Create x and y indices
        y_ind, x_ind = np.indices(star_seg.shape)

        # Estimate saturation level from image type
        saturation = (2**(8 * star_seg.itemsize) - 1) * np.ones_like(y_ind)

        # Fit a PSF to the star
        try:
            # Fit the 2D Gaussian with the limited number of iterations - this reduces the processing time
            # and most of the bad star candidates take more iterations to fit
            popt, pcov = opt.curve_fit(twoDGaussian, (y_ind, x_ind, saturation), star_seg.ravel(), \
                p0=initial_guess, maxfev=200)
            # print(popt)
        except RuntimeError:
            # print('Fitting failed!')

            # Skip stars that can't be fitted in 200 iterations
            continue

        # Unpack fitted gaussian parameters
        amplitude, yo, xo, sigma_y, sigma_x, theta, offset = popt

        # Filter hot pixels by looking at the ratio between x and y sigmas (HPs are very narrow)
        if min(sigma_y / sigma_x, sigma_x / sigma_y) < roundness_threshold:
            # Skip if it is a hot pixel
            continue

        # Reject the star candidate if it is too large
        if (4 * sigma_x * sigma_y / segment_radius**2 > max_feature_ratio):
            continue

        ### If the fitting was successfull, compute the star intensity

        # Crop the star segment to take 3 sigma portion around the star
        crop_y_min = int(yo - 3 * sigma_y) + 1
        if crop_y_min < 0: crop_y_min = 0

        crop_y_max = int(yo + 3 * sigma_y) + 1
        if crop_y_max >= star_seg.shape[0]: crop_y_max = star_seg.shape[0] - 1

        crop_x_min = int(xo - 3 * sigma_x) + 1
        if crop_x_min < 0: crop_x_min = 0

        crop_x_max = int(xo + 3 * sigma_x) + 1
        if crop_x_max >= star_seg.shape[1]: crop_x_max = star_seg.shape[1] - 1

        # If the segment is too small, set a fixed size
        if (y_max - y_min) < 3:
            crop_y_min = int(yo - 2)
            crop_y_max = int(yo + 2)

        if (x_max - x_min) < 3:
            crop_x_min = int(xo - 2)
            crop_x_max = int(xo + 2)

        star_seg_crop = star_seg[crop_y_min:crop_y_max, crop_x_min:crop_x_max]

        # Skip the star if the shape is too small
        if (star_seg_crop.shape[0] == 0) or (star_seg_crop.shape[1] == 0):
            continue

        # Gamma correct the star segment
        star_seg_crop = Image.gammaCorrection(star_seg_crop.astype(np.float32),
                                              config.gamma)

        # Correct the background for gamma
        bg_corrected = Image.gammaCorrection(offset, config.gamma)

        # Subtract the background from the star segment and compute the total intensity
        intensity = np.sum(star_seg_crop - bg_corrected)

        # Skip stars with zero intensity
        if intensity <= 0:
            continue

        # print(intensity)
        # plt.imshow(star_seg_crop - bg_corrected, cmap='gray', vmin=0, vmax=255)
        # plt.show()

        ###

        # Calculate the intensity (as a volume under the 2D Gaussian) (OLD, before gamma correction)
        # intensity = 2*np.pi*amplitude*sigma_x*sigma_y

        # # Skip if the star intensity is below background level
        # if intensity < offset:
        #     continue

        # Add stars to the final list
        x_fitted.append(x_min + xo)
        y_fitted.append(y_min + yo)
        amplitude_fitted.append(amplitude)
        intensity_fitted.append(intensity)
        sigma_y_fitted.append(sigma_y)
        sigma_x_fitted.append(sigma_x)

        # # Plot fitted stars
        # data_fitted = twoDGaussian((y_ind, x_ind), *popt) - offset

        # fig, ax = plt.subplots(1, 1)
        # ax.hold(True)
        # plt.title('Center Y: '+str(y_min[0])+', X:'+str(x_min[0]))
        # ax.imshow(star_seg.reshape(segment_radius*2, segment_radius*2), cmap=plt.cm.inferno, origin='bottom',
        #     extent=(x_ind.min(), x_ind.max(), y_ind.min(), y_ind.max()))
        # # ax.imshow(data_fitted.reshape(segment_radius*2, segment_radius*2), cmap=plt.cm.jet, origin='bottom')
        # ax.contour(x_ind, y_ind, data_fitted.reshape(segment_radius*2, segment_radius*2), 8, colors='w')

        # plt.show()
        # plt.clf()
        # plt.close()

    return x_fitted, y_fitted, amplitude_fitted, intensity_fitted, sigma_y_fitted, sigma_x_fitted
def generateCalibrationReport(config, night_dir_path, match_radius=2.0, platepar=None, show_graphs=False):
    """ Given the folder of the night, find the Calstars file, check the star fit and generate a report
        with the quality of the calibration. The report contains information about both the astrometry and
        the photometry calibration. Graphs will be saved in the given directory of the night.
    
    Arguments:
        config: [Config instance]
        night_dir_path: [str] Full path to the directory of the night.

    Keyword arguments:
        match_radius: [float] Match radius for star matching between image and catalog stars (px).
        platepar: [Platepar instance] Use this platepar instead of finding one in the folder.
        show_graphs: [bool] Show the graphs on the screen. False by default.

    Return:
        None
    """

    # Find the CALSTARS file in the given folder
    calstars_file = None
    for calstars_file in os.listdir(night_dir_path):
        if ('CALSTARS' in calstars_file) and ('.txt' in calstars_file):
            break

    if calstars_file is None:
        print('CALSTARS file could not be found in the given directory!')
        return None


    # Load the calstars file
    star_list = readCALSTARS(night_dir_path, calstars_file)



    ### Load recalibrated platepars, if they exist ###

    # Find recalibrated platepars file per FF file
    platepars_recalibrated_file = None
    for file_name in os.listdir(night_dir_path):
        if file_name == config.platepars_recalibrated_name:
            platepars_recalibrated_file = file_name
            break


    # Load all recalibrated platepars if the file is available
    recalibrated_platepars = None
    if platepars_recalibrated_file:
        with open(os.path.join(night_dir_path, platepars_recalibrated_file)) as f:
            recalibrated_platepars = json.load(f)
            print('Loaded recalibrated platepars JSON file for the calibration report...')

    ### ###


    ### Load the platepar file ###

    # Find the platepar file in the given directory if it was not given
    if platepar is None:

        # Find the platepar file
        platepar_file = None
        for file_name in os.listdir(night_dir_path):
            if file_name == config.platepar_name:
                platepar_file = file_name
                break

        if platepar_file is None:
            print('The platepar cannot be found in the night directory!')
            return None

        # Load the platepar file
        platepar = Platepar()
        platepar.read(os.path.join(night_dir_path, platepar_file))


    ### ###


    night_name = os.path.split(night_dir_path.strip(os.sep))[1]


    # Go one mag deeper than in the config
    lim_mag = config.catalog_mag_limit + 1

    # Load catalog stars (load one magnitude deeper)
    catalog_stars, mag_band_str, config.star_catalog_band_ratios = StarCatalog.readStarCatalog(\
        config.star_catalog_path, config.star_catalog_file, lim_mag=lim_mag, \
        mag_band_ratios=config.star_catalog_band_ratios)

    
    ### Take only those CALSTARS entires for which FF files exist in the folder ###

    # Get a list of FF files in the folder\
    ff_list = []
    for file_name in os.listdir(night_dir_path):
        if validFFName(file_name):
            ff_list.append(file_name)


    # Filter out calstars entries, generate a star dictionary where the keys are JDs of FFs
    star_dict = {}
    ff_dict = {}
    for entry in star_list:

        ff_name, star_data = entry

        # Check if the FF from CALSTARS exists in the folder
        if ff_name not in ff_list:
            continue


        dt = getMiddleTimeFF(ff_name, config.fps, ret_milliseconds=True)
        jd = date2JD(*dt)

        # Add the time and the stars to the dict
        star_dict[jd] = star_data
        ff_dict[jd] = ff_name

    ### ###

    # If there are no FF files in the directory, don't generate a report
    if len(star_dict) == 0:
        print('No FF files from the CALSTARS file in the directory!')
        return None


    # If the recalibrated platepars file exists, take the one with the most stars
    max_jd = 0
    if recalibrated_platepars is not None:
        max_stars = 0
        for ff_name_temp in recalibrated_platepars:

            # Compute the Julian date of the FF middle
            dt = getMiddleTimeFF(ff_name_temp, config.fps, ret_milliseconds=True)
            jd = date2JD(*dt)

            # Check that this file exists in CALSTARS and the list of FF files
            if (jd not in star_dict) or (jd not in ff_dict):
                continue

            # Check if the number of stars on this FF file is larger than the before
            if len(star_dict[jd]) > max_stars:
                max_jd = jd
                max_stars = len(star_dict[jd])


        # Set a flag to indicate if using recalibrated platepars has failed
        if max_jd == 0:
            using_recalib_platepars = False
        else:

            print('Using recalibrated platepars, file:', ff_dict[max_jd])
            using_recalib_platepars = True

            # Select the platepar where the FF file has the most stars
            platepar_dict = recalibrated_platepars[ff_dict[max_jd]]
            platepar = Platepar()
            platepar.loadFromDict(platepar_dict)

            filtered_star_dict = {max_jd: star_dict[max_jd]}

            # Match stars on the image with the stars in the catalog
            n_matched, avg_dist, cost, matched_stars = matchStarsResiduals(config, platepar, catalog_stars, \
                filtered_star_dict, match_radius, ret_nmatch=True, lim_mag=lim_mag)

            max_matched_stars = n_matched


    # Otherwise take the optimal FF file for evaluation
    if (recalibrated_platepars is None) or (not using_recalib_platepars):

        # If there are more than a set number of FF files to evaluate, choose only the ones with most stars on
        #   the image
        if len(star_dict) > config.calstars_files_N:

            # Find JDs of FF files with most stars on them
            top_nstars_indices = np.argsort([len(x) for x in star_dict.values()])[::-1][:config.calstars_files_N \
                - 1]

            filtered_star_dict = {}
            for i in top_nstars_indices:
                filtered_star_dict[list(star_dict.keys())[i]] = list(star_dict.values())[i]

            star_dict = filtered_star_dict


        # Match stars on the image with the stars in the catalog
        n_matched, avg_dist, cost, matched_stars = matchStarsResiduals(config, platepar, catalog_stars, \
            star_dict, match_radius, ret_nmatch=True, lim_mag=lim_mag)



    # If no recalibrated platepars where found, find the image with the largest number of matched stars
    if (not using_recalib_platepars) or (max_jd == 0):

        max_jd = 0
        max_matched_stars = 0
        for jd in matched_stars:
            _, _, distances = matched_stars[jd]
            if len(distances) > max_matched_stars:
                max_jd = jd
                max_matched_stars = len(distances)

        
        # If there are no matched stars, use the image with the largest number of detected stars
        if max_matched_stars <= 2:
            max_jd = max(star_dict, key=lambda x: len(star_dict[x]))
            distances = [np.inf]



    # Take the FF file with the largest number of matched stars
    ff_name = ff_dict[max_jd]

    # Load the FF file
    ff = readFF(night_dir_path, ff_name)
    img_h, img_w = ff.avepixel.shape

    dpi = 200
    plt.figure(figsize=(ff.avepixel.shape[1]/dpi, ff.avepixel.shape[0]/dpi), dpi=dpi)

    # Take the average pixel
    img = ff.avepixel

    # Slightly adjust the levels
    img = Image.adjustLevels(img, np.percentile(img, 1.0), 1.2, np.percentile(img, 99.99))

    plt.imshow(img, cmap='gray', interpolation='nearest')

    legend_handles = []


    # Plot detected stars
    for img_star in star_dict[max_jd]:

        y, x, _, _ = img_star

        rect_side = 5*match_radius
        square_patch = plt.Rectangle((x - rect_side/2, y - rect_side/2), rect_side, rect_side, color='g', \
            fill=False, label='Image stars')

        plt.gca().add_artist(square_patch)

    legend_handles.append(square_patch)



    # If there are matched stars, plot them
    if max_matched_stars > 2:

        # Take the solution with the largest number of matched stars
        image_stars, matched_catalog_stars, distances = matched_stars[max_jd]

        # Plot matched stars
        for img_star in image_stars:
            x, y, _, _ = img_star

            circle_patch = plt.Circle((y, x), radius=3*match_radius, color='y', fill=False, \
                label='Matched stars')

            plt.gca().add_artist(circle_patch)

        legend_handles.append(circle_patch)


        ### Plot match residuals ###

        # Compute preducted positions of matched image stars from the catalog
        x_predicted, y_predicted = raDecToXYPP(matched_catalog_stars[:, 0], \
            matched_catalog_stars[:, 1], max_jd, platepar)

        img_y, img_x, _, _ = image_stars.T

        delta_x = x_predicted - img_x
        delta_y = y_predicted - img_y

        # Compute image residual and angle of the error
        res_angle = np.arctan2(delta_y, delta_x)
        res_distance = np.sqrt(delta_x**2 + delta_y**2)


        # Calculate coordinates of the beginning of the residual line
        res_x_beg = img_x + 3*match_radius*np.cos(res_angle)
        res_y_beg = img_y + 3*match_radius*np.sin(res_angle)

        # Calculate coordinates of the end of the residual line
        res_x_end = img_x + 100*np.cos(res_angle)*res_distance
        res_y_end = img_y + 100*np.sin(res_angle)*res_distance

        # Plot the 100x residuals
        for i in range(len(x_predicted)):
            res_plot = plt.plot([res_x_beg[i], res_x_end[i]], [res_y_beg[i], res_y_end[i]], color='orange', \
                lw=0.5, label='100x residuals')

        legend_handles.append(res_plot[0])

        ### ###

    else:

        distances = [np.inf]
        
        # If there are no matched stars, plot large text in the middle of the screen
        plt.text(img_w/2, img_h/2, "NO MATCHED STARS!", color='r', alpha=0.5, fontsize=20, ha='center',
            va='center')


    ### Plot positions of catalog stars to the limiting magnitude of the faintest matched star + 1 mag ###

    # Find the faintest magnitude among matched stars
    if max_matched_stars > 2:
        faintest_mag = np.max(matched_catalog_stars[:, 2]) + 1

    else:
        # If there are no matched stars, use the limiting magnitude from config
        faintest_mag = config.catalog_mag_limit + 1


    # Estimate RA,dec of the centre of the FOV
    _, RA_c, dec_c, _ = xyToRaDecPP([jd2Date(max_jd)], [platepar.X_res/2], [platepar.Y_res/2], [1], 
        platepar)

    RA_c = RA_c[0]
    dec_c = dec_c[0]

    fov_radius = np.hypot(*computeFOVSize(platepar))

    # Get stars from the catalog around the defined center in a given radius
    _, extracted_catalog = subsetCatalog(catalog_stars, RA_c, dec_c, fov_radius, faintest_mag)
    ra_catalog, dec_catalog, mag_catalog = extracted_catalog.T

    # Compute image positions of all catalog stars that should be on the image
    x_catalog, y_catalog = raDecToXYPP(ra_catalog, dec_catalog, max_jd, platepar)

    # Filter all catalog stars outside the image
    temp_arr = np.c_[x_catalog, y_catalog, mag_catalog]
    temp_arr = temp_arr[temp_arr[:, 0] >= 0]
    temp_arr = temp_arr[temp_arr[:, 0] <= ff.avepixel.shape[1]]
    temp_arr = temp_arr[temp_arr[:, 1] >= 0]
    temp_arr = temp_arr[temp_arr[:, 1] <= ff.avepixel.shape[0]]
    x_catalog, y_catalog, mag_catalog = temp_arr.T

    # Plot catalog stars on the image
    cat_stars_handle = plt.scatter(x_catalog, y_catalog, c='none', marker='D', lw=1.0, alpha=0.4, \
        s=((4.0 + (faintest_mag - mag_catalog))/3.0)**(2*2.512), edgecolor='r', label='Catalog stars')

    legend_handles.append(cat_stars_handle)

    ### ###


    # Add info text
    info_text = ff_dict[max_jd] + '\n' \
        + "Matched stars: {:d}/{:d}\n".format(max_matched_stars, len(star_dict[max_jd])) \
        + "Median distance: {:.2f} px\n".format(np.median(distances)) \
        + "Catalog limiting magnitude: {:.1f}".format(lim_mag)

    plt.text(10, 10, info_text, bbox=dict(facecolor='black', alpha=0.5), va='top', ha='left', fontsize=4, \
        color='w')

    legend = plt.legend(handles=legend_handles, prop={'size': 4}, loc='upper right')
    legend.get_frame().set_facecolor('k')
    legend.get_frame().set_edgecolor('k')
    for txt in legend.get_texts():
        txt.set_color('w')


    plt.axis('off')
    plt.gca().get_xaxis().set_visible(False)
    plt.gca().get_yaxis().set_visible(False)

    plt.xlim([0, ff.avepixel.shape[1]])
    plt.ylim([ff.avepixel.shape[0], 0])

    # Remove the margins
    plt.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)

    plt.savefig(os.path.join(night_dir_path, night_name + '_calib_report_astrometry.jpg'), \
        bbox_inches='tight', pad_inches=0, dpi=dpi)


    if show_graphs:
        plt.show()

    else:
        plt.clf()
        plt.close()



    if max_matched_stars > 2:

        ### Plot the photometry ###

        plt.figure(dpi=dpi)

        # Take only those stars which are inside the 3/4 of the shorter image axis from the center
        photom_selection_radius = np.min([img_h, img_w])/3
        filter_indices = ((image_stars[:, 0] - img_h/2)**2 + (image_stars[:, 1] \
            - img_w/2)**2) <= photom_selection_radius**2
        star_intensities = image_stars[filter_indices, 2]
        catalog_mags = matched_catalog_stars[filter_indices, 2]

        # Plot intensities of image stars
        #star_intensities = image_stars[:, 2]
        plt.scatter(-2.5*np.log10(star_intensities), catalog_mags, s=5, c='r')

        # Fit the photometry on automated star intensities
        photom_offset, fit_stddev, _ = photometryFit(np.log10(star_intensities), catalog_mags)


        # Plot photometric offset from the platepar
        x_min, x_max = plt.gca().get_xlim()
        y_min, y_max = plt.gca().get_ylim()

        x_min_w = x_min - 3
        x_max_w = x_max + 3
        y_min_w = y_min - 3
        y_max_w = y_max + 3

        photometry_info = 'Platepar: {:+.2f}LSP {:+.2f} +/- {:.2f} \nGamma = {:.2f}'.format(platepar.mag_0, \
            platepar.mag_lev, platepar.mag_lev_stddev, platepar.gamma)

        # Plot the photometry calibration from the platepar
        logsum_arr = np.linspace(x_min_w, x_max_w, 10)
        plt.plot(logsum_arr, logsum_arr + platepar.mag_lev, label=photometry_info, linestyle='--', \
            color='k', alpha=0.5)

        # Plot the fitted photometry calibration
        fit_info = "Fit: {:+.2f}LSP {:+.2f} +/- {:.2f}".format(-2.5, photom_offset, fit_stddev)
        plt.plot(logsum_arr, logsum_arr + photom_offset, label=fit_info, linestyle='--', color='red', 
            alpha=0.5)

        plt.legend()

        plt.ylabel("Catalog magnitude ({:s})".format(mag_band_str))
        plt.xlabel("Uncalibrated magnitude")

        # Set wider axis limits
        plt.xlim(x_min_w, x_max_w)
        plt.ylim(y_min_w, y_max_w)

        plt.gca().invert_yaxis()
        plt.gca().invert_xaxis()

        plt.grid()

        plt.savefig(os.path.join(night_dir_path, night_name + '_calib_report_photometry.png'), dpi=150)


        if show_graphs:
            plt.show()

        else:
            plt.clf()
            plt.close()
Esempio n. 13
0
    def updateImage(self):
        """ Updates the current plot. """

        # Reset circle patches
        self.circle_aperature = None
        self.circle_aperature_outer = None

        # Reset centroid patch
        self.centroid_handle = None

        # Reset photometry coloring
        self.photometry_coloring_handle = None

        # Save the previous zoom
        if self.current_image is not None:

            self.prev_xlim = plt.gca().get_xlim()
            self.prev_ylim = plt.gca().get_ylim()

        plt.clf()

        # PNG mode
        if self.png_mode:

            # Get path to current PNG image
            self.png_img_path = os.path.join(
                self.png_dir, self.png_list[int(self.current_frame)])

            # Read the image
            img = scipy.misc.imread(self.png_img_path)

        # FF mode
        else:
            # If FF is given, reconstruct frames
            if self.ff is not None:

                # Take the current frame from FF file
                img = np.copy(self.ff.avepixel)
                frame_mask = np.where(
                    self.ff.maxframe == int(self.current_frame))
                img[frame_mask] = self.ff.maxpixel[frame_mask]

            # Otherwise, create a blank background with the size enough to fit the FR bin
            else:

                # Get the maximum extent of the meteor frames
                y_size = max(max(np.array(self.fr.yc[i]) + np.array(self.fr.size[i])//2) for i in \
                    range(self.fr.lines))
                x_size = max(max(np.array(self.fr.xc[i]) + np.array(self.fr.size[i])//2) for i in \
                    range(self.fr.lines))

                # Make the image square
                img_size = max(y_size, x_size)

                img = np.zeros((img_size, img_size), np.uint8)

            # If FR is given, paste the raw frame onto the image
            if self.fr is not None:

                # Compute the index of the frame in the FR bin structure
                frame_indx = int(
                    self.current_frame) - self.fr.t[self.current_line][0]

                # Reconstruct the frame if it is within the bounds
                if (frame_indx < self.fr.frameNum[self.current_line]) and (
                        frame_indx >= 0):

                    # Get the center position of the detection on the current frame
                    yc = self.fr.yc[self.current_line][frame_indx]
                    xc = self.fr.xc[self.current_line][frame_indx]

                    # # Get the frame number
                    # t = self.fr.t[self.current_line][frame_indx]

                    # Get the size of the window
                    size = self.fr.size[self.current_line][frame_indx]

                    # Paste the frames onto the big image
                    y_img = np.arange(yc - size // 2, yc + size // 2)
                    x_img = np.arange(xc - size // 2, xc + size // 2)

                    Y_img, X_img = np.meshgrid(y_img, x_img)

                    y_frame = np.arange(len(y_img))
                    x_frame = np.arange(len(x_img))

                    Y_frame, X_frame = np.meshgrid(y_frame, x_frame)

                    img[Y_img, X_img] = self.fr.frames[
                        self.current_line][frame_indx][Y_frame, X_frame]

                    # Save the limits of the FR
                    self.fr_xmin = np.min(x_img)
                    self.fr_xmax = np.max(x_img)
                    self.fr_ymin = np.max(y_img)
                    self.fr_ymax = np.min(y_img)

                    # Draw a red rectangle around the pasted frame
                    rect_x = np.min(x_img)
                    rect_y = np.max(y_img)
                    rect_w = np.max(x_img) - rect_x
                    rect_h = np.min(y_img) - rect_y
                    plt.gca().add_patch(mpatches.Rectangle((rect_x, rect_y), rect_w, rect_h, fill=None, \
                        edgecolor='red', alpha=0.5))

        # Apply the deinterlace
        if self.deinterlace_mode > -1:

            # Set the deinterlace index to handle proper deinterlacing order
            if self.deinterlace_mode == 0:
                deinter_indx = 0

            else:
                deinter_indx = 1

            # Deinterlace the image using the appropriate method
            if (self.current_frame + deinter_indx * 0.5) % 1 == 0:
                img = Image.deinterlaceOdd(img)

            else:
                img = Image.deinterlaceEven(img)

        # Current image without adjustments
        self.current_image = np.copy(img)

        ### Adjust image levels

        # Guess the bit depth from the array type
        bit_depth = 8 * img.itemsize

        img = Image.adjustLevels(img, 0, self.img_gamma, (2**bit_depth - 1),
                                 bit_depth)

        ###

        plt.imshow(img, cmap='gray', vmin=0, vmax=255)

        if (self.prev_xlim is not None) and (self.prev_ylim is not None):

            # Restore previous zoom
            plt.xlim(self.prev_xlim)
            plt.ylim(self.prev_ylim)

        self.drawText()

        # Don't draw the picks in the photometry coloring more
        if not self.photometry_coloring_mode:

            # Plot image pick
            self.drawPicks(update_plot=False)

        # Plot the photometry coloring
        self.drawPhotometryColoring(update_plot=False)

        plt.gcf().canvas.draw()
Esempio n. 14
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
Esempio n. 15
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. 16
0
def extractStars(ff_dir,
                 ff_name,
                 config=None,
                 max_global_intensity=150,
                 border=10,
                 neighborhood_size=10,
                 intensity_threshold=5,
                 flat_struct=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.

    Return:
        x2, y2, background, intensity: [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
    """

    # 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)

    # Load the mask file
    mask = MaskImage.loadMask(config.mask_file)

    # Mask the FF file
    ff = MaskImage.applyMask(ff, mask, ff_flag=True)

    # Apply the flat to maxpixel and avepixel
    if flat_struct is not None:

        ff.maxpixel = Image.applyFlat(ff.maxpixel, flat_struct)
        ff.avepixel = Image.applyFlat(ff.avepixel, flat_struct)

    # 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 [[], [], [], []]

    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, (True, border_mask))

    # 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 [[], [], [], []]

    # 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 = fitPSF(ff, global_mean, x, y, config=config)
    # x2, y2, amplitude, intensity = list(x), list(y), [], [] # Skip PSF fit

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

    return x2, y2, amplitude, intensity
Esempio n. 17
0

    # Check if there are any file in the directory
    if(len(ff_list) == None):
        print("No files found!")
        sys.exit()


    # Try loading a flat field image
    flat_struct = None

    if config.use_flat:
        
        # Check if there is flat in the data directory
        if os.path.exists(os.path.join(ff_dir, config.flat_file)):
            flat_struct = Image.loadFlat(ff_dir, config.flat_file)

        # Try loading the default flat
        elif os.path.exists(config.flat_file):
            flat_struct = Image.loadFlat(os.getcwd(), config.flat_file)


    # Initialize the detector
    detector = QueuedPool(detectStarsAndMeteors, cores=-1, log=log)

    # Give detector jobs
    for ff_name in ff_list:
        detector.addJob([ff_dir, ff_name, config, flat_struct])


    # Start the detection
Esempio n. 18
0
    # Get paths to every FF bin file in a directory
    ff_list = [ff for ff in os.listdir(dir_path) if FFfile.validFFName(ff)]

    # Check if there are any file in the directory
    if (len(ff_list) == None):
        print("No files found!")
        sys.exit()

    # Try loading a flat field image
    flat_struct = None

    if config.use_flat:

        # Check if there is flat in the data directory
        if os.path.exists(os.path.join(dir_path, config.flat_file)):
            flat_struct = Image.loadFlat(dir_path, config.flat_file)

        # Try loading the default flat
        elif os.path.exists(config.flat_file):
            flat_struct = Image.loadFlat(os.getcwd(), config.flat_file)

    # Init results list
    results_list = []

    # Open a file for results
    results_path = os.path.abspath(dir_path) + os.sep
    results_name = results_path.split(os.sep)[-2]
    results_file = open(results_path + results_name + '_results.txt', 'w')

    total_meteors = 0
Esempio n. 19
0
def generateCalibrationReport(config,
                              night_dir_path,
                              match_radius=2.0,
                              platepar=None,
                              show_graphs=False):
    """ Given the folder of the night, find the Calstars file, check the star fit and generate a report
        with the quality of the calibration. The report contains information about both the astrometry and
        the photometry calibration. Graphs will be saved in the given directory of the night.
    
    Arguments:
        config: [Config instance]
        night_dir_path: [str] Full path to the directory of the night.

    Keyword arguments:
        match_radius: [float] Match radius for star matching between image and catalog stars (px).
        platepar: [Platepar instance] Use this platepar instead of finding one in the folder.
        show_graphs: [bool] Show the graphs on the screen. False by default.

    Return:
        None
    """

    # Find the CALSTARS file in the given folder
    calstars_file = None
    for calstars_file in os.listdir(night_dir_path):
        if ('CALSTARS' in calstars_file) and ('.txt' in calstars_file):
            break

    if calstars_file is None:
        print('CALSTARS file could not be found in the given directory!')
        return None

    # Load the calstars file
    star_list = readCALSTARS(night_dir_path, calstars_file)

    ### Load recalibrated platepars, if they exist ###

    # Find recalibrated platepars file per FF file
    platepars_recalibrated_file = None
    for file_name in os.listdir(night_dir_path):
        if file_name == config.platepars_recalibrated_name:
            platepars_recalibrated_file = file_name
            break

    # Load all recalibrated platepars if the file is available
    recalibrated_platepars = None
    if platepars_recalibrated_file:
        with open(os.path.join(night_dir_path,
                               platepars_recalibrated_file)) as f:
            recalibrated_platepars = json.load(f)
            print(
                'Loaded recalibrated platepars JSON file for the calibration report...'
            )

    ### ###

    ### Load the platepar file ###

    # Find the platepar file in the given directory if it was not given
    if platepar is None:

        # Find the platepar file
        platepar_file = None
        for file_name in os.listdir(night_dir_path):
            if file_name == config.platepar_name:
                platepar_file = file_name
                break

        if platepar_file is None:
            print('The platepar cannot be found in the night directory!')
            return None

        # Load the platepar file
        platepar = Platepar()
        platepar.read(os.path.join(night_dir_path, platepar_file),
                      use_flat=config.use_flat)

    ### ###

    night_name = os.path.split(night_dir_path.strip(os.sep))[1]

    # Go one mag deeper than in the config
    lim_mag = config.catalog_mag_limit + 1

    # Load catalog stars (load one magnitude deeper)
    catalog_stars, mag_band_str, config.star_catalog_band_ratios = StarCatalog.readStarCatalog(\
        config.star_catalog_path, config.star_catalog_file, lim_mag=lim_mag, \
        mag_band_ratios=config.star_catalog_band_ratios)

    ### Take only those CALSTARS entires for which FF files exist in the folder ###

    # Get a list of FF files in the folder
    ff_list = []
    for file_name in os.listdir(night_dir_path):
        if validFFName(file_name):
            ff_list.append(file_name)

    # Filter out calstars entries, generate a star dictionary where the keys are JDs of FFs
    star_dict = {}
    ff_dict = {}
    for entry in star_list:

        ff_name, star_data = entry

        # Check if the FF from CALSTARS exists in the folder
        if ff_name not in ff_list:
            continue

        dt = getMiddleTimeFF(ff_name, config.fps, ret_milliseconds=True)
        jd = date2JD(*dt)

        # Add the time and the stars to the dict
        star_dict[jd] = star_data
        ff_dict[jd] = ff_name

    ### ###

    # If there are no FF files in the directory, don't generate a report
    if len(star_dict) == 0:
        print('No FF files from the CALSTARS file in the directory!')
        return None

    # If the recalibrated platepars file exists, take the one with the most stars
    max_jd = 0
    using_recalib_platepars = False
    if recalibrated_platepars is not None:
        max_stars = 0
        for ff_name_temp in recalibrated_platepars:

            # Compute the Julian date of the FF middle
            dt = getMiddleTimeFF(ff_name_temp,
                                 config.fps,
                                 ret_milliseconds=True)
            jd = date2JD(*dt)

            # Check that this file exists in CALSTARS and the list of FF files
            if (jd not in star_dict) or (jd not in ff_dict):
                continue

            # Check if the number of stars on this FF file is larger than the before
            if len(star_dict[jd]) > max_stars:
                max_jd = jd
                max_stars = len(star_dict[jd])

        # Set a flag to indicate if using recalibrated platepars has failed
        if max_jd == 0:
            using_recalib_platepars = False
        else:

            print('Using recalibrated platepars, file:', ff_dict[max_jd])
            using_recalib_platepars = True

            # Select the platepar where the FF file has the most stars
            platepar_dict = recalibrated_platepars[ff_dict[max_jd]]
            platepar = Platepar()
            platepar.loadFromDict(platepar_dict, use_flat=config.use_flat)

            filtered_star_dict = {max_jd: star_dict[max_jd]}

            # Match stars on the image with the stars in the catalog
            n_matched, avg_dist, cost, matched_stars = matchStarsResiduals(config, platepar, catalog_stars, \
                filtered_star_dict, match_radius, ret_nmatch=True, lim_mag=lim_mag)

            max_matched_stars = n_matched

    # Otherwise take the optimal FF file for evaluation
    if (recalibrated_platepars is None) or (not using_recalib_platepars):

        # If there are more than a set number of FF files to evaluate, choose only the ones with most stars on
        #   the image
        if len(star_dict) > config.calstars_files_N:

            # Find JDs of FF files with most stars on them
            top_nstars_indices = np.argsort([len(x) for x in star_dict.values()])[::-1][:config.calstars_files_N \
                - 1]

            filtered_star_dict = {}
            for i in top_nstars_indices:
                filtered_star_dict[list(star_dict.keys())[i]] = list(
                    star_dict.values())[i]

            star_dict = filtered_star_dict

        # Match stars on the image with the stars in the catalog
        n_matched, avg_dist, cost, matched_stars = matchStarsResiduals(config, platepar, catalog_stars, \
            star_dict, match_radius, ret_nmatch=True, lim_mag=lim_mag)

    # If no recalibrated platepars where found, find the image with the largest number of matched stars
    if (not using_recalib_platepars) or (max_jd == 0):

        max_jd = 0
        max_matched_stars = 0
        for jd in matched_stars:
            _, _, distances = matched_stars[jd]
            if len(distances) > max_matched_stars:
                max_jd = jd
                max_matched_stars = len(distances)

        # If there are no matched stars, use the image with the largest number of detected stars
        if max_matched_stars <= 2:
            max_jd = max(star_dict, key=lambda x: len(star_dict[x]))
            distances = [np.inf]

    # Take the FF file with the largest number of matched stars
    ff_name = ff_dict[max_jd]

    # Load the FF file
    ff = readFF(night_dir_path, ff_name)
    img_h, img_w = ff.avepixel.shape

    dpi = 200
    plt.figure(figsize=(ff.avepixel.shape[1] / dpi,
                        ff.avepixel.shape[0] / dpi),
               dpi=dpi)

    # Take the average pixel
    img = ff.avepixel

    # Slightly adjust the levels
    img = Image.adjustLevels(img, np.percentile(img, 1.0), 1.3,
                             np.percentile(img, 99.99))

    plt.imshow(img, cmap='gray', interpolation='nearest')

    legend_handles = []

    # Plot detected stars
    for img_star in star_dict[max_jd]:

        y, x, _, _ = img_star

        rect_side = 5 * match_radius
        square_patch = plt.Rectangle((x - rect_side/2, y - rect_side/2), rect_side, rect_side, color='g', \
            fill=False, label='Image stars')

        plt.gca().add_artist(square_patch)

    legend_handles.append(square_patch)

    # If there are matched stars, plot them
    if max_matched_stars > 2:

        # Take the solution with the largest number of matched stars
        image_stars, matched_catalog_stars, distances = matched_stars[max_jd]

        # Plot matched stars
        for img_star in image_stars:
            x, y, _, _ = img_star

            circle_patch = plt.Circle((y, x), radius=3*match_radius, color='y', fill=False, \
                label='Matched stars')

            plt.gca().add_artist(circle_patch)

        legend_handles.append(circle_patch)

        ### Plot match residuals ###

        # Compute preducted positions of matched image stars from the catalog
        x_predicted, y_predicted = raDecToXYPP(matched_catalog_stars[:, 0], \
            matched_catalog_stars[:, 1], max_jd, platepar)

        img_y, img_x, _, _ = image_stars.T

        delta_x = x_predicted - img_x
        delta_y = y_predicted - img_y

        # Compute image residual and angle of the error
        res_angle = np.arctan2(delta_y, delta_x)
        res_distance = np.sqrt(delta_x**2 + delta_y**2)

        # Calculate coordinates of the beginning of the residual line
        res_x_beg = img_x + 3 * match_radius * np.cos(res_angle)
        res_y_beg = img_y + 3 * match_radius * np.sin(res_angle)

        # Calculate coordinates of the end of the residual line
        res_x_end = img_x + 100 * np.cos(res_angle) * res_distance
        res_y_end = img_y + 100 * np.sin(res_angle) * res_distance

        # Plot the 100x residuals
        for i in range(len(x_predicted)):
            res_plot = plt.plot([res_x_beg[i], res_x_end[i]], [res_y_beg[i], res_y_end[i]], color='orange', \
                lw=0.5, label='100x residuals')

        legend_handles.append(res_plot[0])

        ### ###

    else:

        distances = [np.inf]

        # If there are no matched stars, plot large text in the middle of the screen
        plt.text(img_w / 2,
                 img_h / 2,
                 "NO MATCHED STARS!",
                 color='r',
                 alpha=0.5,
                 fontsize=20,
                 ha='center',
                 va='center')

    ### Plot positions of catalog stars to the limiting magnitude of the faintest matched star + 1 mag ###

    # Find the faintest magnitude among matched stars
    if max_matched_stars > 2:
        faintest_mag = np.max(matched_catalog_stars[:, 2]) + 1

    else:
        # If there are no matched stars, use the limiting magnitude from config
        faintest_mag = config.catalog_mag_limit + 1

    # Estimate RA,dec of the centre of the FOV
    _, RA_c, dec_c, _ = xyToRaDecPP([jd2Date(max_jd)], [platepar.X_res / 2],
                                    [platepar.Y_res / 2], [1], platepar)

    RA_c = RA_c[0]
    dec_c = dec_c[0]

    fov_radius = np.hypot(*computeFOVSize(platepar))

    # Get stars from the catalog around the defined center in a given radius
    _, extracted_catalog = subsetCatalog(catalog_stars, RA_c, dec_c,
                                         fov_radius, faintest_mag)
    ra_catalog, dec_catalog, mag_catalog = extracted_catalog.T

    # Compute image positions of all catalog stars that should be on the image
    x_catalog, y_catalog = raDecToXYPP(ra_catalog, dec_catalog, max_jd,
                                       platepar)

    # Filter all catalog stars outside the image
    temp_arr = np.c_[x_catalog, y_catalog, mag_catalog]
    temp_arr = temp_arr[temp_arr[:, 0] >= 0]
    temp_arr = temp_arr[temp_arr[:, 0] <= ff.avepixel.shape[1]]
    temp_arr = temp_arr[temp_arr[:, 1] >= 0]
    temp_arr = temp_arr[temp_arr[:, 1] <= ff.avepixel.shape[0]]
    x_catalog, y_catalog, mag_catalog = temp_arr.T

    # Plot catalog stars on the image
    cat_stars_handle = plt.scatter(x_catalog, y_catalog, c='none', marker='D', lw=1.0, alpha=0.4, \
        s=((4.0 + (faintest_mag - mag_catalog))/3.0)**(2*2.512), edgecolor='r', label='Catalog stars')

    legend_handles.append(cat_stars_handle)

    ### ###

    # Add info text in the corner
    info_text = ff_dict[max_jd] + '\n' \
        + "Matched stars within {:.1f} px radius: {:d}/{:d} \n".format(match_radius, max_matched_stars, \
            len(star_dict[max_jd])) \
        + "Median distance = {:.2f} px\n".format(np.median(distances)) \
        + "Catalog lim mag = {:.1f}".format(lim_mag)

    plt.text(10, 10, info_text, bbox=dict(facecolor='black', alpha=0.5), va='top', ha='left', fontsize=4, \
        color='w', family='monospace')

    legend = plt.legend(handles=legend_handles,
                        prop={'size': 4},
                        loc='upper right')
    legend.get_frame().set_facecolor('k')
    legend.get_frame().set_edgecolor('k')
    for txt in legend.get_texts():
        txt.set_color('w')

    ### Add FOV info (centre, size) ###

    # Mark FOV centre
    plt.scatter(platepar.X_res / 2,
                platepar.Y_res / 2,
                marker='+',
                s=20,
                c='r',
                zorder=4)

    # Compute FOV centre alt/az
    azim_centre, alt_centre = raDec2AltAz(max_jd, platepar.lon, platepar.lat,
                                          RA_c, dec_c)

    # Compute FOV size
    fov_h, fov_v = computeFOVSize(platepar)

    # Compute the rotation wrt. horizon
    rot_horizon = rotationWrtHorizon(platepar)

    fov_centre_text = "Azim  = {:6.2f}$\\degree$\n".format(azim_centre) \
                    + "Alt   = {:6.2f}$\\degree$\n".format(alt_centre) \
                    + "Rot h = {:6.2f}$\\degree$\n".format(rot_horizon) \
                    + "FOV h = {:6.2f}$\\degree$\n".format(fov_h) \
                    + "FOV v = {:6.2f}$\\degree$".format(fov_v) \

    plt.text(10, platepar.Y_res - 10, fov_centre_text, bbox=dict(facecolor='black', alpha=0.5), \
        va='bottom', ha='left', fontsize=4, color='w', family='monospace')

    ### ###

    # Plot RA/Dec gridlines #
    addEquatorialGrid(plt, platepar, max_jd)

    plt.axis('off')
    plt.gca().get_xaxis().set_visible(False)
    plt.gca().get_yaxis().set_visible(False)

    plt.xlim([0, ff.avepixel.shape[1]])
    plt.ylim([ff.avepixel.shape[0], 0])

    # Remove the margins
    plt.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)

    plt.savefig(os.path.join(night_dir_path, night_name + '_calib_report_astrometry.jpg'), \
        bbox_inches='tight', pad_inches=0, dpi=dpi)

    if show_graphs:
        plt.show()

    else:
        plt.clf()
        plt.close()

    if max_matched_stars > 2:

        ### PHOTOMETRY FIT ###

        # If a flat is used, set the vignetting coeff to 0
        if config.use_flat:
            platepar.vignetting_coeff = 0.0

        # Extact intensities and mangitudes
        star_intensities = image_stars[:, 2]
        catalog_mags = matched_catalog_stars[:, 2]

        # Compute radius of every star from image centre
        radius_arr = np.hypot(image_stars[:, 0] - img_h / 2,
                              image_stars[:, 1] - img_w / 2)

        # Fit the photometry on automated star intensities (use the fixed vignetting coeff, use robust fit)
        photom_params, fit_stddev, fit_resid, star_intensities, radius_arr, catalog_mags = \
            photometryFitRobust(star_intensities, radius_arr, catalog_mags, \
            fixed_vignetting=platepar.vignetting_coeff)

        photom_offset, _ = photom_params

        ### ###

        ### PLOT PHOTOMETRY ###
        # Note: An almost identical code exists in RMS.Astrometry.SkyFit in the PlateTool.photometry function

        dpi = 130
        fig_p, (ax_p, ax_r) = plt.subplots(nrows=2, facecolor=None, figsize=(6.0, 7.0), dpi=dpi, \
            gridspec_kw={'height_ratios':[2, 1]})

        # Plot raw star intensities
        ax_p.scatter(-2.5 * np.log10(star_intensities),
                     catalog_mags,
                     s=5,
                     c='r',
                     alpha=0.5,
                     label="Raw")

        # If a flat is used, disregard the vignetting
        if not config.use_flat:

            # Plot intensities of image stars corrected for vignetting
            lsp_corr_arr = np.log10(correctVignetting(star_intensities, radius_arr, \
                platepar.vignetting_coeff))
            ax_p.scatter(-2.5*lsp_corr_arr, catalog_mags, s=5, c='b', alpha=0.5, \
                label="Corrected for vignetting")

        # Plot photometric offset from the platepar
        x_min, x_max = ax_p.get_xlim()
        y_min, y_max = ax_p.get_ylim()

        x_min_w = x_min - 3
        x_max_w = x_max + 3
        y_min_w = y_min - 3
        y_max_w = y_max + 3

        photometry_info = "Platepar: {:+.1f}*LSP + {:.2f} +/- {:.2f}".format(platepar.mag_0, \
            platepar.mag_lev, platepar.mag_lev_stddev) \
            + "\nVignetting coeff = {:.5f}".format(platepar.vignetting_coeff) \
            + "\nGamma = {:.2f}".format(platepar.gamma)

        # Plot the photometry calibration from the platepar
        logsum_arr = np.linspace(x_min_w, x_max_w, 10)
        ax_p.plot(logsum_arr, logsum_arr + platepar.mag_lev, label=photometry_info, linestyle='--', \
            color='k', alpha=0.5)

        # Plot the fitted photometry calibration
        fit_info = "Fit: {:+.1f}*LSP + {:.2f} +/- {:.2f}".format(
            -2.5, photom_offset, fit_stddev)
        ax_p.plot(logsum_arr,
                  logsum_arr + photom_offset,
                  label=fit_info,
                  linestyle='--',
                  color='b',
                  alpha=0.75)

        ax_p.legend()

        ax_p.set_ylabel("Catalog magnitude ({:s})".format(mag_band_str))
        ax_p.set_xlabel("Uncalibrated magnitude")

        # Set wider axis limits
        ax_p.set_xlim(x_min_w, x_max_w)
        ax_p.set_ylim(y_min_w, y_max_w)

        ax_p.invert_yaxis()
        ax_p.invert_xaxis()

        ax_p.grid()

        ### Plot photometry vs radius ###

        img_diagonal = np.hypot(img_h / 2, img_w / 2)

        # Plot photometry residuals (including vignetting)
        ax_r.scatter(radius_arr, fit_resid, c='b', alpha=0.75, s=5, zorder=3)

        # Plot a zero line
        ax_r.plot(np.linspace(0, img_diagonal, 10), np.zeros(10), linestyle='dashed', alpha=0.5, \
            color='k')

        # Plot only when no flat is used
        if not config.use_flat:

            #  Plot radius from centre vs. fit residual
            fit_resids_novignetting = catalog_mags - photomLine((np.array(star_intensities), \
                np.array(radius_arr)), photom_offset, 0.0)
            ax_r.scatter(radius_arr,
                         fit_resids_novignetting,
                         s=5,
                         c='r',
                         alpha=0.5,
                         zorder=3)

            px_sum_tmp = 1000
            radius_arr_tmp = np.linspace(0, img_diagonal, 50)

            # Plot vignetting loss curve
            vignetting_loss = 2.5*np.log10(px_sum_tmp) \
                - 2.5*np.log10(correctVignetting(px_sum_tmp, radius_arr_tmp, \
                    platepar.vignetting_coeff))

            ax_r.plot(radius_arr_tmp,
                      vignetting_loss,
                      linestyle='dotted',
                      alpha=0.5,
                      color='k')

        ax_r.grid()

        ax_r.set_ylabel("Fit residuals (mag)")
        ax_r.set_xlabel("Radius from centre (px)")

        ax_r.set_xlim(0, img_diagonal)

        ### ###

        plt.tight_layout()

        plt.savefig(os.path.join(night_dir_path,
                                 night_name + '_calib_report_photometry.png'),
                    dpi=150)

        if show_graphs:
            plt.show()

        else:
            plt.clf()
            plt.close()
Esempio n. 20
0
    # Check if there are any file in the directory
    if(len(ff_list) == None):
        print("No files found!")
        sys.exit()



    # Try loading a flat field image
    flat_struct = None

    if config.use_flat:
        
        # Check if there is flat in the data directory
        if os.path.exists(os.path.join(ff_dir, config.flat_file)):
            flat_struct = Image.loadFlat(ff_dir, config.flat_file)

        # Try loading the default flat
        elif os.path.exists(config.flat_file):
            flat_struct = Image.loadFlat(os.getcwd(), config.flat_file)


    

    extraction_list = []

    # Go through all files in the directory and add them to the detection list
    for ff_name in sorted(ff_list):

        # Check if the given file is a valid FF file
        if not FFfile.validFFName(ff_name):