Esempio n. 1
0
def generateThumbnails(dir_path, config, mosaic_type, file_list=None):
    """ Generates a mosaic of thumbnails from all FF files in the given folder and saves it as a JPG image.
    
    Arguments:
        dir_path: [str] Path of the night directory.
        config: [Conf object] Configuration.
        mosaic_type: [str] Type of the mosaic (e.g. "Captured" or "Detected")

    Keyword arguments:
        file_list: [list] A list of file names (without full path) which will be searched for FF files. This
            is used when generating separate thumbnails for captured and detected files.

    Return:
        file_name: [str] Name of the thumbnail file.

    """

    if file_list is None:
        file_list = sorted(os.listdir(dir_path))


    # Make a list of all FF files in the night directory
    ff_list = []

    for file_name in file_list:
        if FFfile.validFFName(file_name):
            ff_list.append(file_name)


    # Calculate the dimensions of the binned image
    bin_w = int(config.width/config.thumb_bin)
    bin_h = int(config.height/config.thumb_bin)


    ### RESIZE AND STACK THUMBNAILS ###
    ##########################################################################################################

    timestamps = []
    stacked_imgs = []

    for i in range(0, len(ff_list), config.thumb_stack):

        img_stack = np.zeros((bin_h, bin_w))

        # Stack thumb_stack images using the 'if lighter' method
        for j in range(config.thumb_stack):

            if (i + j) < len(ff_list):

                # Read maxpixel image
                img = FFfile.read(dir_path, ff_list[i + j]).maxpixel

                # Resize the image
                img = cv2.resize(img, (bin_w, bin_h))

                # Stack the image
                img_stack = stackIfLighter(img_stack, img)

            else:
                break


        # Save the timestamp of the first image in the stack
        timestamps.append(FFfile.filenameToDatetime(ff_list[i]))

        # Save the stacked image
        stacked_imgs.append(img_stack)

        # cv2.imshow('test', img_stack)
        # cv2.waitKey(0)
        # cv2.destroyAllWindows()



    ##########################################################################################################

    ### ADD THUMBS TO ONE MOSAIC IMAGE ###
    ##########################################################################################################

    header_height = 20
    timestamp_height = 10

    # Calculate the number of rows for the thumbnail image
    n_rows = int(np.ceil(float(len(ff_list))/config.thumb_stack/config.thumb_n_width))

    # Calculate the size of the mosaic
    mosaic_w = int(config.thumb_n_width*bin_w)
    mosaic_h = int((bin_h + timestamp_height)*n_rows + header_height)

    mosaic_img = np.zeros((mosaic_h, mosaic_w), dtype=np.uint8)

    # Write header text
    header_text = 'Station: ' + str(config.stationID) + ' Night: ' + os.path.basename(dir_path) \
        + ' Type: ' + mosaic_type
    cv2.putText(mosaic_img, header_text, (0, header_height//2), \
                    cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255,255,255), 1)

    for row in range(n_rows):

        for col in range(config.thumb_n_width):

            # Calculate image index
            indx = row*config.thumb_n_width + col

            if indx < len(stacked_imgs):

                # Calculate position of the text
                text_x = col*bin_w
                text_y = row*bin_h + (row + 1)*timestamp_height - 1 + header_height

                # Add timestamp text
                cv2.putText(mosaic_img, timestamps[indx].strftime('%H:%M:%S'), (text_x, text_y), \
                    cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1)

                # Add the image to the mosaic
                img_pos_x = col*bin_w
                img_pos_y = row*bin_h + (row + 1)*timestamp_height + header_height

                mosaic_img[img_pos_y : img_pos_y + bin_h, img_pos_x : img_pos_x + bin_w] = stacked_imgs[indx]


            else:
                break

    ##########################################################################################################

    thumb_name = "{:s}_{:s}_{:s}_thumbs.jpg".format(str(config.stationID), os.path.basename(dir_path), \
        mosaic_type)

    # Save the mosaic
    cv2.imwrite(os.path.join(dir_path, thumb_name), mosaic_img, [int(cv2.IMWRITE_JPEG_QUALITY), 80])


    return thumb_name
Esempio n. 2
0
def recalibrateIndividualFFsAndApplyAstrometry(dir_path, ftpdetectinfo_path, calstars_list, config, platepar,
    generate_plot=True):
    """ Recalibrate FF files with detections and apply the recalibrated platepar to those detections. 

    Arguments:
        dir_path: [str] Path where the FTPdetectinfo file is.
        ftpdetectinfo_path: [str] Name of the FTPdetectinfo file.
        calstars_list: [list] A list of entries [[ff_name, star_coordinates], ...].
        config: [Config instance]
        platepar: [Platepar instance] Initial platepar.

    Keyword arguments:
        generate_plot: [bool] Generate the calibration variation plot. True by default.

    Return:
        recalibrated_platepars: [dict] A dictionary where the keys are FF file names and values are 
            recalibrated platepar instances for every FF file.
    """

    # Use a copy of the config file
    config = copy.deepcopy(config)

    # If the given file does not exits, return nothing
    if not os.path.isfile(ftpdetectinfo_path):
        print('ERROR! The FTPdetectinfo file does not exist: {:s}'.format(ftpdetectinfo_path))
        print('    The recalibration on every file was not done!')

        return {}


    # Read the FTPdetectinfo data
    cam_code, fps, meteor_list = FTPdetectinfo.readFTPdetectinfo(*os.path.split(ftpdetectinfo_path), \
        ret_input_format=True)

    # Convert the list of stars to a per FF name dictionary
    calstars = {ff_file: star_data for ff_file, star_data in calstars_list}


    ### Add neighboring FF files for more robust photometry estimation ###

    ff_processing_list = []

    # Make a list of sorted FF files in CALSTARS
    calstars_ffs = sorted([ff_file for ff_file in calstars])

    # Go through the list of FF files with detections and add neighboring FFs
    for meteor_entry in meteor_list:

        ff_name = meteor_entry[0]

        if ff_name in calstars_ffs:

            # Find the index of the given FF file in the list of calstars
            ff_indx = calstars_ffs.index(ff_name)

            # Add neighbours to the processing list
            for k in range(-(RECALIBRATE_NEIGHBOURHOOD_SIZE//2), RECALIBRATE_NEIGHBOURHOOD_SIZE//2 + 1):

                k_indx = ff_indx + k

                if (k_indx > 0) and (k_indx < len(calstars_ffs)):

                    ff_name_tmp = calstars_ffs[k_indx]
                    if ff_name_tmp not in ff_processing_list:
                        ff_processing_list.append(ff_name_tmp)


    # Sort the processing list of FF files
    ff_processing_list = sorted(ff_processing_list)


    ### ###


    # Globally increase catalog limiting magnitude
    config.catalog_mag_limit += 1

    # Load catalog stars (overwrite the mag band ratios if specific catalog is used)
    star_catalog_status = StarCatalog.readStarCatalog(config.star_catalog_path,\
        config.star_catalog_file, lim_mag=config.catalog_mag_limit, \
        mag_band_ratios=config.star_catalog_band_ratios)

    if not star_catalog_status:
        print("Could not load the star catalog!")
        print(os.path.join(config.star_catalog_path, config.star_catalog_file))
        return {}

    catalog_stars, _, config.star_catalog_band_ratios = star_catalog_status


    # Update the platepar coordinates from the config file
    platepar.lat = config.latitude
    platepar.lon = config.longitude
    platepar.elev = config.elevation


    prev_platepar = copy.deepcopy(platepar)

    # Go through all FF files with detections, recalibrate and apply astrometry
    recalibrated_platepars = {}
    for ff_name in ff_processing_list:

        working_platepar = copy.deepcopy(prev_platepar)

        # Skip this meteor if its FF file was already recalibrated
        if ff_name in recalibrated_platepars:
            continue

        print()
        print('Processing: ', ff_name)
        print('------------------------------------------------------------------------------')

        # Find extracted stars on this image
        if not ff_name in calstars:
            print('Skipped because it was not in CALSTARS:', ff_name)
            continue

        # Get stars detected on this FF file (create a dictionaly with only one entry, the residuals function
        #   needs this format)
        calstars_time = FFfile.getMiddleTimeFF(ff_name, config.fps, ret_milliseconds=True)
        jd = date2JD(*calstars_time)
        star_dict_ff = {jd: calstars[ff_name]}

        # Recalibrate the platepar using star matching
        result, min_match_radius = recalibrateFF(config, working_platepar, jd, star_dict_ff, catalog_stars)

        
        # If the recalibration failed, try using FFT alignment
        if result is None:

            print()
            print('Running FFT alignment...')

            # Run FFT alignment
            calstars_coords = np.array(star_dict_ff[jd])[:, :2]
            calstars_coords[:, [0, 1]] = calstars_coords[:, [1, 0]]
            print(calstars_time)
            test_platepar = alignPlatepar(config, prev_platepar, calstars_time, calstars_coords, \
                show_plot=False)

            # Try to recalibrate after FFT alignment
            result, _ = recalibrateFF(config, test_platepar, jd, star_dict_ff, catalog_stars)


            # If the FFT alignment failed, align the original platepar using the smallest radius that matched
            #   and force save the the platepar
            if (result is None) and (min_match_radius is not None):
                print()
                print("Using the old platepar with the minimum match radius of: {:.2f}".format(min_match_radius))
                result, _ = recalibrateFF(config, working_platepar, jd, star_dict_ff, catalog_stars, 
                    max_match_radius=min_match_radius, force_platepar_save=True)

                if result is not None:
                    working_platepar = result


            # If the alignment succeeded, save the result
            else:
                working_platepar = result


        else:
            working_platepar = result


        # Store the platepar if the fit succeeded
        if result is not None:

            # Recompute alt/az of the FOV centre
            working_platepar.az_centre, working_platepar.alt_centre = raDec2AltAz(working_platepar.RA_d, \
                working_platepar.dec_d, working_platepar.JD, working_platepar.lat, working_platepar.lon)

            # Recompute the rotation wrt horizon
            working_platepar.rotation_from_horiz = rotationWrtHorizon(working_platepar)

            # Mark the platepar to indicate that it was automatically recalibrated on an individual FF file
            working_platepar.auto_recalibrated = True

            recalibrated_platepars[ff_name] = working_platepar
            prev_platepar = working_platepar

        else:

            print('Recalibration of {:s} failed, using the previous platepar...'.format(ff_name))

            # Mark the platepar to indicate that autorecalib failed
            prev_platepar_tmp = copy.deepcopy(prev_platepar)
            prev_platepar_tmp.auto_recalibrated = False

            # If the aligning failed, set the previous platepar as the one that should be used for this FF file
            recalibrated_platepars[ff_name] = prev_platepar_tmp



    ### Average out photometric offsets within the given neighbourhood size ###

    # Go through the list of FF files with detections
    for meteor_entry in meteor_list:

        ff_name = meteor_entry[0]

        # Make sure the FF was successfuly recalibrated
        if ff_name in recalibrated_platepars:

            # Find the index of the given FF file in the list of calstars
            ff_indx = calstars_ffs.index(ff_name)

            # Compute the average photometric offset and the improved standard deviation using all
            #   neighbors
            photom_offset_tmp_list = []
            photom_offset_std_tmp_list = []
            neighboring_ffs = []
            for k in range(-(RECALIBRATE_NEIGHBOURHOOD_SIZE//2), RECALIBRATE_NEIGHBOURHOOD_SIZE//2 + 1):

                k_indx = ff_indx + k

                if (k_indx > 0) and (k_indx < len(calstars_ffs)):

                    # Get the name of the FF file
                    ff_name_tmp = calstars_ffs[k_indx]

                    # Check that the neighboring FF was successfuly recalibrated
                    if ff_name_tmp in recalibrated_platepars:
                        
                        # Get the computed photometric offset and stddev
                        photom_offset_tmp_list.append(recalibrated_platepars[ff_name_tmp].mag_lev)
                        photom_offset_std_tmp_list.append(recalibrated_platepars[ff_name_tmp].mag_lev_stddev)
                        neighboring_ffs.append(ff_name_tmp)


            # Compute the new photometric offset and improved standard deviation (assume equal sample size)
            #   Source: https://stats.stackexchange.com/questions/55999/is-it-possible-to-find-the-combined-standard-deviation
            photom_offset_new = np.mean(photom_offset_tmp_list)
            photom_offset_std_new = np.sqrt(\
                np.sum([st**2 + (mt - photom_offset_new)**2 \
                for mt, st in zip(photom_offset_tmp_list, photom_offset_std_tmp_list)]) \
                / len(photom_offset_tmp_list)
                )

            # Assign the new photometric offset and standard deviation to all FFs used for computation
            for ff_name_tmp in neighboring_ffs:
                recalibrated_platepars[ff_name_tmp].mag_lev = photom_offset_new
                recalibrated_platepars[ff_name_tmp].mag_lev_stddev = photom_offset_std_new

    ### ###


    ### Store all recalibrated platepars as a JSON file ###

    all_pps = {}
    for ff_name in recalibrated_platepars:

        json_str = recalibrated_platepars[ff_name].jsonStr()
        
        all_pps[ff_name] = json.loads(json_str)

    with open(os.path.join(dir_path, config.platepars_recalibrated_name), 'w') as f:
        
        # Convert all platepars to a JSON file
        out_str = json.dumps(all_pps, default=lambda o: o.__dict__, indent=4, sort_keys=True)

        f.write(out_str)

    ### ###



    # If no platepars were recalibrated, use the single platepar recalibration procedure
    if len(recalibrated_platepars) == 0:

        print('No FF images were used for recalibration, using the single platepar calibration function...')

        # Use the initial platepar for calibration
        applyAstrometryFTPdetectinfo(dir_path, os.path.basename(ftpdetectinfo_path), None, platepar=platepar)

        return recalibrated_platepars



    ### GENERATE PLOTS ###

    dt_list = []
    ang_dists = []
    rot_angles = []
    hour_list = []
    photom_offset_list = []
    photom_offset_std_list = []

    first_dt = np.min([FFfile.filenameToDatetime(ff_name) for ff_name in recalibrated_platepars])

    for ff_name in recalibrated_platepars:
        
        pp_temp = recalibrated_platepars[ff_name]

        # If the fitting failed, skip the platepar
        if pp_temp is None:
            continue

        # Add the datetime of the FF file to the list
        ff_dt = FFfile.filenameToDatetime(ff_name)
        dt_list.append(ff_dt)


        # Compute the angular separation from the reference platepar
        ang_dist = np.degrees(angularSeparation(np.radians(platepar.RA_d), np.radians(platepar.dec_d), \
            np.radians(pp_temp.RA_d), np.radians(pp_temp.dec_d)))
        ang_dists.append(ang_dist*60)

        # Compute rotation difference
        rot_diff = (platepar.pos_angle_ref - pp_temp.pos_angle_ref + 180)%360 - 180
        rot_angles.append(rot_diff*60)

        # Compute the hour of the FF used for recalibration
        hour_list.append((ff_dt - first_dt).total_seconds()/3600)

        # Add the photometric offset to the list
        photom_offset_list.append(pp_temp.mag_lev)
        photom_offset_std_list.append(pp_temp.mag_lev_stddev)



    if generate_plot:

        # Generate the name the plots
        plot_name = os.path.basename(ftpdetectinfo_path).replace('FTPdetectinfo_', '').replace('.txt', '')

        
        ### Plot difference from reference platepar in angular distance from (0, 0) vs rotation ###    

        plt.figure()

        plt.scatter(0, 0, marker='o', edgecolor='k', label='Reference platepar', s=100, c='none', zorder=3)

        plt.scatter(ang_dists, rot_angles, c=hour_list, zorder=3)
        plt.colorbar(label="Hours from first FF file")
        
        plt.xlabel("Angular distance from reference (arcmin)")
        plt.ylabel("Rotation from reference (arcmin)")

        plt.title("FOV centre drift starting at {:s}".format(first_dt.strftime("%Y/%m/%d %H:%M:%S")))

        plt.grid()
        plt.legend()

        plt.tight_layout()            

        plt.savefig(os.path.join(dir_path, plot_name + '_calibration_variation.png'), dpi=150)

        # plt.show()

        plt.clf()
        plt.close()

        ### ###


        ### Plot the photometric offset variation ###

        plt.figure()

        plt.errorbar(dt_list, photom_offset_list, yerr=photom_offset_std_list, fmt="o", \
            ecolor='lightgray', elinewidth=2, capsize=0, ms=2)

        # Format datetimes
        plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))

        # rotate and align the tick labels so they look better
        plt.gcf().autofmt_xdate()

        plt.xlabel("UTC time")
        plt.ylabel("Photometric offset")

        plt.title("Photometric offset variation")

        plt.grid()

        plt.tight_layout()

        plt.savefig(os.path.join(dir_path, plot_name + '_photometry_variation.png'), dpi=150)

        plt.clf()
        plt.close()

    ### ###



    ### Apply platepars to FTPdetectinfo ###

    meteor_output_list = []
    for meteor_entry in meteor_list:

        ff_name, meteor_No, rho, phi, meteor_meas = meteor_entry

        # Get the platepar that will be applied to this FF file
        if ff_name in recalibrated_platepars:
            working_platepar = recalibrated_platepars[ff_name]

        else:
            print('Using default platepar for:', ff_name)
            working_platepar = platepar

        # Apply the recalibrated platepar to meteor centroids
        meteor_picks = applyPlateparToCentroids(ff_name, fps, meteor_meas, working_platepar, \
            add_calstatus=True)

        meteor_output_list.append([ff_name, meteor_No, rho, phi, meteor_picks])


    # Calibration string to be written to the FTPdetectinfo file
    calib_str = 'Recalibrated with RMS on: ' + str(datetime.datetime.utcnow()) + ' UTC'

    # If no meteors were detected, set dummpy parameters
    if len(meteor_list) == 0:
        cam_code = ''
        fps = 0


    # Back up the old FTPdetectinfo file
    try:
        shutil.copy(ftpdetectinfo_path, ftpdetectinfo_path.strip('.txt') \
            + '_backup_{:s}.txt'.format(datetime.datetime.utcnow().strftime('%Y%m%d_%H%M%S.%f')))
    except:
        print('ERROR! The FTPdetectinfo file could not be backed up: {:s}'.format(ftpdetectinfo_path))

    # Save the updated FTPdetectinfo
    FTPdetectinfo.writeFTPdetectinfo(meteor_output_list, dir_path, os.path.basename(ftpdetectinfo_path), \
        dir_path, cam_code, fps, calibration=calib_str, celestial_coords_given=True)


    ### ###

    return recalibrated_platepars
Esempio n. 3
0
def generateThumbnails(dir_path,
                       config,
                       mosaic_type,
                       file_list=None,
                       no_stack=False):
    """ Generates a mosaic of thumbnails from all FF files in the given folder and saves it as a JPG image.
    
    Arguments:
        dir_path: [str] Path of the night directory.
        config: [Conf object] Configuration.
        mosaic_type: [str] Type of the mosaic (e.g. "Captured" or "Detected")

    Keyword arguments:
        file_list: [list] A list of file names (without full path) which will be searched for FF files. This
            is used when generating separate thumbnails for captured and detected files.

    Return:
        file_name: [str] Name of the thumbnail file.
        no_stack: [bool] Don't stack the images using the config.thumb_stack option. A max of 1000 images
            are supported with this option. If there are more, stacks will be done according to the 
            config.thumb_stack option.

    """

    if file_list is None:
        file_list = sorted(os.listdir(dir_path))

    # Make a list of all FF files in the night directory
    ff_list = []

    for file_name in file_list:
        if FFfile.validFFName(file_name):
            ff_list.append(file_name)

    # Calculate the dimensions of the binned image
    bin_w = int(config.width / config.thumb_bin)
    bin_h = int(config.height / config.thumb_bin)

    ### RESIZE AND STACK THUMBNAILS ###
    ##########################################################################################################

    timestamps = []
    stacked_imgs = []

    thumb_stack = config.thumb_stack

    # Check if no stacks should be done (max 1000 images for no stack)
    if no_stack and (len(ff_list) < 1000):
        thumb_stack = 1

    for i in range(0, len(ff_list), thumb_stack):

        img_stack = np.zeros((bin_h, bin_w))

        # Stack thumb_stack images using the 'if lighter' method
        for j in range(thumb_stack):

            if (i + j) < len(ff_list):

                tmp_file_name = ff_list[i + j]

                # Read the FF file
                ff = FFfile.read(dir_path, tmp_file_name)

                # Skip the FF if it is corruped
                if ff is None:
                    continue

                img = ff.maxpixel

                # Resize the image
                img = cv2.resize(img, (bin_w, bin_h))

                # Stack the image
                img_stack = stackIfLighter(img_stack, img)

            else:
                break

        # Save the timestamp of the first image in the stack
        timestamps.append(FFfile.filenameToDatetime(ff_list[i]))

        # Save the stacked image
        stacked_imgs.append(img_stack)

        # cv2.imshow('test', img_stack)
        # cv2.waitKey(0)
        # cv2.destroyAllWindows()

    ##########################################################################################################

    ### ADD THUMBS TO ONE MOSAIC IMAGE ###
    ##########################################################################################################

    header_height = 20
    timestamp_height = 10

    # Calculate the number of rows for the thumbnail image
    n_rows = int(
        np.ceil(float(len(ff_list)) / thumb_stack / config.thumb_n_width))

    # Calculate the size of the mosaic
    mosaic_w = int(config.thumb_n_width * bin_w)
    mosaic_h = int((bin_h + timestamp_height) * n_rows + header_height)

    mosaic_img = np.zeros((mosaic_h, mosaic_w), dtype=np.uint8)

    # Write header text
    header_text = 'Station: ' + str(config.stationID) + ' Night: ' + os.path.basename(dir_path) \
        + ' Type: ' + mosaic_type
    cv2.putText(mosaic_img, header_text, (0, header_height//2), \
                    cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255,255,255), 1)

    for row in range(n_rows):

        for col in range(config.thumb_n_width):

            # Calculate image index
            indx = row * config.thumb_n_width + col

            if indx < len(stacked_imgs):

                # Calculate position of the text
                text_x = col * bin_w
                text_y = row * bin_h + (
                    row + 1) * timestamp_height - 1 + header_height

                # Add timestamp text
                cv2.putText(mosaic_img, timestamps[indx].strftime('%H:%M:%S'), (text_x, text_y), \
                    cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1)

                # Add the image to the mosaic
                img_pos_x = col * bin_w
                img_pos_y = row * bin_h + (
                    row + 1) * timestamp_height + header_height

                mosaic_img[img_pos_y:img_pos_y + bin_h,
                           img_pos_x:img_pos_x + bin_w] = stacked_imgs[indx]

            else:
                break

    ##########################################################################################################

    # Only add the station ID if the dir name already doesn't start with it
    dir_name = os.path.basename(os.path.abspath(dir_path))
    if dir_name.startswith(config.stationID):
        prefix = dir_name
    else:
        prefix = "{:s}_{:s}".format(config.stationID, dir_name)

    thumb_name = "{:s}_{:s}_thumbs.jpg".format(prefix, mosaic_type)

    # Save the mosaic
    if USING_IMAGEIO:
        # Use imageio to write the image
        imwrite(os.path.join(dir_path, thumb_name), mosaic_img, quality=80)
    else:
        # Use OpenCV to save the image
        imwrite(os.path.join(dir_path, thumb_name), mosaic_img,
                [int(cv2.IMWRITE_JPEG_QUALITY), 80])

    return thumb_name
def generateThumbnails(dir_path, config, mosaic_type, file_list=None):
    """ Generates a mosaic of thumbnails from all FF files in the given folder and saves it as a JPG image.
    
    Arguments:
        dir_path: [str] Path of the night directory.
        config: [Conf object] Configuration.
        mosaic_type: [str] Type of the mosaic (e.g. "Captured" or "Detected")

    Keyword arguments:
        file_list: [list] A list of file names (without full path) which will be searched for FF files. This
            is used when generating separate thumbnails for captured and detected files.

    Return:
        file_name: [str] Name of the thumbnail file.

    """

    if file_list is None:
        file_list = sorted(os.listdir(dir_path))


    # Make a list of all FF files in the night directory
    ff_list = []

    for file_name in file_list:
        if FFfile.validFFName(file_name):
            ff_list.append(file_name)


    # Calculate the dimensions of the binned image
    bin_w = int(config.width/config.thumb_bin)
    bin_h = int(config.height/config.thumb_bin)


    ### RESIZE AND STACK THUMBNAILS ###
    ##########################################################################################################

    timestamps = []
    stacked_imgs = []

    for i in range(0, len(ff_list), config.thumb_stack):

        img_stack = np.zeros((bin_h, bin_w))

        # Stack thumb_stack images using the 'if lighter' method
        for j in range(config.thumb_stack):

            if (i + j) < len(ff_list):

                tmp_file_name = ff_list[i + j]

                    
                # Read the FF file
                ff = FFfile.read(dir_path, tmp_file_name)

                # Skip the FF if it is corruped
                if ff is None:
                    continue

                img = ff.maxpixel

                # Resize the image
                img = cv2.resize(img, (bin_w, bin_h))

                # Stack the image
                img_stack = stackIfLighter(img_stack, img)

            else:
                break


        # Save the timestamp of the first image in the stack
        timestamps.append(FFfile.filenameToDatetime(ff_list[i]))

        # Save the stacked image
        stacked_imgs.append(img_stack)

        # cv2.imshow('test', img_stack)
        # cv2.waitKey(0)
        # cv2.destroyAllWindows()



    ##########################################################################################################

    ### ADD THUMBS TO ONE MOSAIC IMAGE ###
    ##########################################################################################################

    header_height = 20
    timestamp_height = 10

    # Calculate the number of rows for the thumbnail image
    n_rows = int(np.ceil(float(len(ff_list))/config.thumb_stack/config.thumb_n_width))

    # Calculate the size of the mosaic
    mosaic_w = int(config.thumb_n_width*bin_w)
    mosaic_h = int((bin_h + timestamp_height)*n_rows + header_height)

    mosaic_img = np.zeros((mosaic_h, mosaic_w), dtype=np.uint8)

    # Write header text
    header_text = 'Station: ' + str(config.stationID) + ' Night: ' + os.path.basename(dir_path) \
        + ' Type: ' + mosaic_type
    cv2.putText(mosaic_img, header_text, (0, header_height//2), \
                    cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255,255,255), 1)

    for row in range(n_rows):

        for col in range(config.thumb_n_width):

            # Calculate image index
            indx = row*config.thumb_n_width + col

            if indx < len(stacked_imgs):

                # Calculate position of the text
                text_x = col*bin_w
                text_y = row*bin_h + (row + 1)*timestamp_height - 1 + header_height

                # Add timestamp text
                cv2.putText(mosaic_img, timestamps[indx].strftime('%H:%M:%S'), (text_x, text_y), \
                    cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1)

                # Add the image to the mosaic
                img_pos_x = col*bin_w
                img_pos_y = row*bin_h + (row + 1)*timestamp_height + header_height

                mosaic_img[img_pos_y : img_pos_y + bin_h, img_pos_x : img_pos_x + bin_w] = stacked_imgs[indx]


            else:
                break

    ##########################################################################################################

    thumb_name = "{:s}_{:s}_{:s}_thumbs.jpg".format(str(config.stationID), os.path.basename(dir_path), \
        mosaic_type)

    # Save the mosaic
    cv2.imwrite(os.path.join(dir_path, thumb_name), mosaic_img, [int(cv2.IMWRITE_JPEG_QUALITY), 80])


    return thumb_name
Esempio n. 5
0
def FTPdetectinfo2UFOOrbitInput(dir_path, file_name, platepar_path, platepar_dict=None):
    """ Convert the FTPdetectinfo file into UFOOrbit input CSV file. 
        
    Arguments:
        dir_path: [str] Path of the directory which contains the FTPdetectinfo file.
        file_name: [str] Name of the FTPdetectinfo file.
        platepar_path: [str] Full path to the platepar file.

    Keyword arguments:
        platepar_dict: [dict] Dictionary of Platepar instances where keys are FF file names. This will be 
            used instead of the platepar at platepar_path. None by default.
    """

    # Load the FTPdetecinfo file
    meteor_list = FTPdetectinfo.readFTPdetectinfo(dir_path, file_name)


    # Load the platepar file
    if platepar_dict is None:

        pp = RMS.Formats.Platepar.Platepar()
        pp.read(platepar_path)


    # Init the UFO format list
    ufo_meteor_list = []

    # Go through every meteor in the list
    for meteor in meteor_list:

        ff_name, cam_code, meteor_No, n_segments, fps, hnr, mle, binn, px_fm, rho, phi, \
            meteor_meas = meteor

        # Load the platepar from the platepar dictionary, if given
        if platepar_dict is not None:
            if ff_name in platepar_dict:
                pp = platepar_dict[ff_name]

            else:
                print('Skipping {:s} becuase no platepar was found for this FF file!'.format(ff_name))

        # Convert the FF file name into time
        dt = FFfile.filenameToDatetime(ff_name)

        # Extract measurements
        calib_status, frame_n, x, y, ra, dec, azim, elev, inten, mag = np.array(meteor_meas).T

        # If the meteor wasn't calibrated, skip it
        if not np.all(calib_status):
            print('Meteor {:d} was not calibrated, skipping it...'.format(meteor_No))
            continue

        # Compute the peak magnitude
        peak_mag = np.min(mag)

        # Compute the total duration
        first_frame = np.min(frame_n)
        last_frame = np.max(frame_n) 
        duration = (last_frame - first_frame)/fps


        # Compute times of first and last points
        dt1 = dt + datetime.timedelta(seconds=first_frame/fps)
        dt2 = dt + datetime.timedelta(seconds=last_frame/fps)

        
        ### Fit a great circle to Az/Alt measurements and compute model beg/end RA and Dec ###

        # Convert the measurement Az/Alt to cartesian coordinates
        # NOTE: All values that are used for Great Circle computation are:
        #   theta - the zenith angle (90 deg - altitude)
        #   phi - azimuth +N of due E, which is (90 deg - azim)
        x, y, z = Math.polarToCartesian(np.radians((90 - azim)%360), np.radians(90 - elev))

        # Fit a great circle
        C, theta0, phi0 = GreatCircle.fitGreatCircle(x, y, z)

        # Get the first point on the great circle
        phase1 = GreatCircle.greatCirclePhase(np.radians(90 - elev[0]), np.radians((90 - azim[0])%360), \
            theta0, phi0)
        alt1, azim1 = Math.cartesianToPolar(*GreatCircle.greatCircle(phase1, theta0, phi0))
        alt1 = 90 - np.degrees(alt1)
        azim1 = (90 - np.degrees(azim1))%360



        # Get the last point on the great circle
        phase2 = GreatCircle.greatCirclePhase(np.radians(90 - elev[-1]), np.radians((90 - azim[-1])%360),\
            theta0, phi0)
        alt2, azim2 = Math.cartesianToPolar(*GreatCircle.greatCircle(phase2, theta0, phi0))
        alt2 = 90 - np.degrees(alt2)
        azim2 = (90 - np.degrees(azim2))%360

        # Compute RA/Dec from Alt/Az
        _, ra1, dec1 = RMS.Astrometry.ApplyAstrometry.altAzToRADec(pp.lat, pp.lon, pp.UT_corr, [dt1], \
            [azim1], [alt1], dt_time=True)
        _, ra2, dec2 = RMS.Astrometry.ApplyAstrometry.altAzToRADec(pp.lat, pp.lon, pp.UT_corr, [dt2], \
            [azim2], [alt2], dt_time=True)


        ### ###


        ufo_meteor_list.append([dt1, peak_mag, duration, azim1[0], alt1[0], azim2[0], alt2[0], \
            ra1[0][0], dec1[0][0], ra2[0][0], dec2[0][0], cam_code, pp.lon, pp.lat, pp.elev, pp.UT_corr])


    # Construct a file name for the UFO file, which is the FTPdetectinfo file without the FTPdetectinfo 
    #   part
    ufo_file_name = file_name.replace('FTPdetectinfo_', '').replace('.txt', '') + '.csv'

    # Write the UFOorbit file
    UFOOrbit.writeUFOOrbit(dir_path, ufo_file_name, ufo_meteor_list)
Esempio n. 6
0
def FTPdetectinfo2UFOOrbitInput(dir_path,
                                file_name,
                                platepar_path,
                                platepar_dict=None):
    """ Convert the FTPdetectinfo file into UFOOrbit input CSV file. 
        
    Arguments:
        dir_path: [str] Path of the directory which contains the FTPdetectinfo file.
        file_name: [str] Name of the FTPdetectinfo file.
        platepar_path: [str] Full path to the platepar file.

    Keyword arguments:
        platepar_dict: [dict] Dictionary of Platepar instances where keys are FF file names. This will be 
            used instead of the platepar at platepar_path. None by default.
    """

    # Load the FTPdetecinfo file
    meteor_list = FTPdetectinfo.readFTPdetectinfo(dir_path, file_name)

    # Load the platepar file
    if platepar_dict is None:

        pp = RMS.Formats.Platepar.Platepar()
        pp.read(platepar_path, use_flat=None)

    # Init the UFO format list
    ufo_meteor_list = []

    # Go through every meteor in the list
    for meteor in meteor_list:

        ff_name, cam_code, meteor_No, n_segments, fps, hnr, mle, binn, px_fm, rho, phi, \
            meteor_meas = meteor

        # Load the platepar from the platepar dictionary, if given
        if platepar_dict is not None:
            if ff_name in platepar_dict:
                pp = platepar_dict[ff_name]

            else:
                print(
                    'Skipping {:s} becuase no platepar was found for this FF file!'
                    .format(ff_name))
                continue

        # Convert the FF file name into time
        dt = FFfile.filenameToDatetime(ff_name)

        # Extract measurements
        calib_status, frame_n, x, y, ra, dec, azim, elev, inten, mag = np.array(
            meteor_meas).T

        # If the meteor wasn't calibrated, skip it
        if not np.all(calib_status):
            print('Meteor {:d} was not calibrated, skipping it...'.format(
                meteor_No))
            continue

        # Compute the peak magnitude
        peak_mag = np.min(mag)

        # Compute the total duration
        first_frame = np.min(frame_n)
        last_frame = np.max(frame_n)
        duration = (last_frame - first_frame) / fps

        # Compute times of first and last points
        dt1 = dt + datetime.timedelta(seconds=first_frame / fps)
        dt2 = dt + datetime.timedelta(seconds=last_frame / fps)

        ### Fit a great circle to Az/Alt measurements and compute model beg/end RA and Dec ###

        # Convert the measurement Az/Alt to cartesian coordinates
        # NOTE: All values that are used for Great Circle computation are:
        #   theta - the zenith angle (90 deg - altitude)
        #   phi - azimuth +N of due E, which is (90 deg - azim)
        x, y, z = Math.polarToCartesian(np.radians((90 - azim) % 360),
                                        np.radians(90 - elev))

        # Fit a great circle
        C, theta0, phi0 = GreatCircle.fitGreatCircle(x, y, z)

        # Get the first point on the great circle
        phase1 = GreatCircle.greatCirclePhase(np.radians(90 - elev[0]), np.radians((90 - azim[0])%360), \
            theta0, phi0)
        alt1, azim1 = Math.cartesianToPolar(
            *GreatCircle.greatCircle(phase1, theta0, phi0))
        alt1 = 90 - np.degrees(alt1)
        azim1 = (90 - np.degrees(azim1)) % 360

        # Get the last point on the great circle
        phase2 = GreatCircle.greatCirclePhase(np.radians(90 - elev[-1]), np.radians((90 - azim[-1])%360),\
            theta0, phi0)
        alt2, azim2 = Math.cartesianToPolar(
            *GreatCircle.greatCircle(phase2, theta0, phi0))
        alt2 = 90 - np.degrees(alt2)
        azim2 = (90 - np.degrees(azim2)) % 360

        # Compute RA/Dec from Alt/Az
        _, ra1, dec1 = RMS.Astrometry.ApplyAstrometry.altAzToRADec(pp.lat, pp.lon, pp.UT_corr, [dt1], \
            [azim1], [alt1], dt_time=True)
        _, ra2, dec2 = RMS.Astrometry.ApplyAstrometry.altAzToRADec(pp.lat, pp.lon, pp.UT_corr, [dt2], \
            [azim2], [alt2], dt_time=True)

        ### ###


        ufo_meteor_list.append([dt1, peak_mag, duration, azim1[0], alt1[0], azim2[0], alt2[0], \
            ra1[0][0], dec1[0][0], ra2[0][0], dec2[0][0], cam_code, pp.lon, pp.lat, pp.elev, pp.UT_corr])

    # Construct a file name for the UFO file, which is the FTPdetectinfo file without the FTPdetectinfo
    #   part
    ufo_file_name = file_name.replace('FTPdetectinfo_', '').replace(
        '.txt', '') + '.csv'

    # Write the UFOorbit file
    UFOOrbit.writeUFOOrbit(dir_path, ufo_file_name, ufo_meteor_list)
def recalibrateIndividualFFsAndApplyAstrometry(dir_path, ftpdetectinfo_path, calstars_list, config, platepar):
    """ Recalibrate FF files with detections and apply the recalibrated platepar to those detections. 

    Arguments:
        dir_path: [str] Path where the FTPdetectinfo file is.
        ftpdetectinfo_path: [str] Name of the FTPdetectinfo file.
        calstars_list: [list] A list of entries [[ff_name, star_coordinates], ...].
        config: [Config instance]
        platepar: [Platepar instance] Initial platepar.

    Return:
        recalibrated_platepars: [dict] A dictionary where the keys are FF file names and values are 
            recalibrated platepar instances for every FF file.
    """


    # Read the FTPdetectinfo data
    cam_code, fps, meteor_list = FTPdetectinfo.readFTPdetectinfo(*os.path.split(ftpdetectinfo_path), \
        ret_input_format=True)

    # Convert the list of stars to a per FF name dictionary
    calstars = {ff_file: star_data for ff_file, star_data in calstars_list}


    # Load catalog stars (overwrite the mag band ratios if specific catalog is used)
    catalog_stars, _, config.star_catalog_band_ratios = StarCatalog.readStarCatalog(config.star_catalog_path,\
        config.star_catalog_file, lim_mag=config.catalog_mag_limit, \
        mag_band_ratios=config.star_catalog_band_ratios)



    prev_platepar = copy.deepcopy(platepar)

    # Go through all FF files with detections, recalibrate and apply astrometry
    recalibrated_platepars = {}
    for meteor_entry in meteor_list:

        working_platepar = copy.deepcopy(prev_platepar)

        ff_name, meteor_No, rho, phi, meteor_meas = meteor_entry

        # Skip this meteors if its FF file was already recalibrated
        if ff_name in recalibrated_platepars:
            continue

        print()
        print('Processing: ', ff_name)
        print('------------------------------------------------------------------------------')

        # Find extracted stars on this image
        if not ff_name in calstars:
            print('Skipped because it was not in CALSTARS:', ff_name)
            continue

        # Get stars detected on this FF file (create a dictionaly with only one entry, the residuals function
        #   needs this format)
        calstars_time = FFfile.getMiddleTimeFF(ff_name, config.fps, ret_milliseconds=True)
        jd = date2JD(*calstars_time)
        star_dict_ff = {jd: calstars[ff_name]}

        # Recalibrate the platepar using star matching
        result = recalibrateFF(config, working_platepar, jd, star_dict_ff, catalog_stars)

        
        # If the recalibration failed, try using FFT alignment
        if result is None:

            print()
            print('Running FFT alignment...')

            # Run FFT alignment
            calstars_coords = np.array(star_dict_ff[jd])[:, :2]
            calstars_coords[:, [0, 1]] = calstars_coords[:, [1, 0]]
            print(calstars_time)
            working_platepar = alignPlatepar(config, prev_platepar, calstars_time, calstars_coords, \
                show_plot=False)

            # Try to recalibrate after FFT alignment
            result = recalibrateFF(config, working_platepar, jd, star_dict_ff, catalog_stars)

            if result is not None:
                working_platepar = result


        else:
            working_platepar = result


        # Store the platepar if the fit succeeded
        if result is not None:
            recalibrated_platepars[ff_name] = working_platepar
            prev_platepar = working_platepar

        else:

            print('Recalibration of {:s} failed, using the previous platepar...'.format(ff_name))

            # If the aligning failed, set the previous platepar as the one that should be used for this FF file
            recalibrated_platepars[ff_name] = prev_platepar


    ### Store all recalibrated platepars as a JSON file ###

    all_pps = {}
    for ff_name in recalibrated_platepars:

        json_str = recalibrated_platepars[ff_name].jsonStr()
        
        all_pps[ff_name] = json.loads(json_str)

    with open(os.path.join(dir_path, config.platepars_recalibrated_name), 'w') as f:
        
        # Convert all platepars to a JSON file
        out_str = json.dumps(all_pps, default=lambda o: o.__dict__, indent=4, sort_keys=True)

        f.write(out_str)

    ### ###



    # If no platepars were recalibrated, use the single platepar recalibration procedure
    if len(recalibrated_platepars) == 0:

        print('No FF images were used for recalibration, using the single platepar calibration function...')

        # Use the initial platepar for calibration
        applyAstrometryFTPdetectinfo(dir_path, os.path.basename(ftpdetectinfo_path), None, platepar=platepar)

        return recalibrated_platepars



    ### Plot difference from reference platepar in angular distance from (0, 0) vs rotation ###

    ang_dists = []
    rot_angles = []
    hour_list = []

    first_jd = np.min([FFfile.filenameToDatetime(ff_name) for ff_name in recalibrated_platepars])

    for ff_name in recalibrated_platepars:
        
        pp_temp = recalibrated_platepars[ff_name]

        # If the fitting failed, skip the platepar
        if pp_temp is None:
            continue

        # Compute the angular separation from the reference platepar
        ang_dist = np.degrees(angularSeparation(np.radians(platepar.RA_d), np.radians(platepar.dec_d), \
            np.radians(pp_temp.RA_d), np.radians(pp_temp.dec_d)))
        ang_dists.append(ang_dist*60)

        rot_angles.append((platepar.pos_angle_ref - pp_temp.pos_angle_ref)*60)

        # Compute the hour of the FF used for recalibration
        hour_list.append((FFfile.filenameToDatetime(ff_name) - first_jd).total_seconds()/3600)


    plt.figure()

    plt.scatter(0, 0, marker='o', edgecolor='k', label='Reference platepar', s=100, c='none', zorder=3)

    plt.scatter(ang_dists, rot_angles, c=hour_list, zorder=3)
    plt.colorbar(label='Hours from first FF file')
    
    plt.xlabel("Angular distance from reference (arcmin)")
    plt.ylabel('Rotation from reference (arcmin)')

    plt.grid()
    plt.legend()

    plt.tight_layout()

    # Generate the name for the plot
    calib_plot_name = os.path.basename(ftpdetectinfo_path).replace('FTPdetectinfo_', '').replace('.txt', '') \
        + '_calibration_variation.png'

    plt.savefig(os.path.join(dir_path, calib_plot_name), dpi=150)

    # plt.show()

    plt.clf()
    plt.close()

    ### ###



    ### Apply platepars to FTPdetectinfo ###

    meteor_output_list = []
    for meteor_entry in meteor_list:

        ff_name, meteor_No, rho, phi, meteor_meas = meteor_entry

        # Get the platepar that will be applied to this FF file
        if ff_name in recalibrated_platepars:
            working_platepar = recalibrated_platepars[ff_name]

        else:
            print('Using default platepar for:', ff_name)
            working_platepar = platepar

        # Apply the recalibrated platepar to meteor centroids
        meteor_picks = applyPlateparToCentroids(ff_name, fps, meteor_meas, working_platepar, \
            add_calstatus=True)

        meteor_output_list.append([ff_name, meteor_No, rho, phi, meteor_picks])


    # Calibration string to be written to the FTPdetectinfo file
    calib_str = 'Recalibrated with RMS on: ' + str(datetime.datetime.utcnow()) + ' UTC'

    # If no meteors were detected, set dummpy parameters
    if len(meteor_list) == 0:
        cam_code = ''
        fps = 0


    # Back up the old FTPdetectinfo file
    shutil.copy(ftpdetectinfo_path, ftpdetectinfo_path.strip('.txt') \
        + '_backup_{:s}.txt'.format(datetime.datetime.utcnow().strftime('%Y%m%d_%H%M%S.%f')))

    # Save the updated FTPdetectinfo
    FTPdetectinfo.writeFTPdetectinfo(meteor_output_list, dir_path, os.path.basename(ftpdetectinfo_path), \
        dir_path, cam_code, fps, calibration=calib_str, celestial_coords_given=True)


    ### ###

    return recalibrated_platepars