def stackFFs(dir_path, file_format, deinterlace=False, subavg=False, filter_bright=False, flat_path=None, file_list=None, mask=None): """ Stack FF files in the given folder. Arguments: dir_path: [str] Path to the directory with FF files. file_format: [str] Image format for the stack. E.g. jpg, png, bmp Keyword arguments: deinterlace: [bool] True if the image shoud be deinterlaced prior to stacking. False by default. subavg: [bool] Whether the average pixel image should be subtracted form the max pixel image. False by default. filter_bright: [bool] Whether images with bright backgrounds (after average subtraction) should be skipped. False by defualt. flat_path: [str] Path to the flat calibration file. None by default. Will only be used if subavg is False. file_list: [list] A list of file for stacking. False by default, in which case all FF files in the given directory will be used. mask: [MaskStructure] Mask to apply to the stack. None by default. Return: stack_path, merge_img: - stack_path: [str] Path of the save stack. - merge_img: [ndarray] Numpy array of the stacked image. """ # Load the flat if it was given flat = None if flat_path != '': # Try finding the default flat if flat_path is None: flat_path = dir_path flat_file = 'flat.bmp' else: flat_path, flat_file = os.path.split(flat_path) flat_full_path = os.path.join(flat_path, flat_file) if os.path.isfile(flat_full_path): # Load the flat flat = loadFlat(flat_path, flat_file) print('Loaded flat:', flat_full_path) first_img = True n_stacked = 0 total_ff_files = 0 merge_img = None # If the list of files was not given, take all files in the given folder if file_list is None: file_list = sorted(os.listdir(dir_path)) # List all FF files in the current dir for ff_name in file_list: if validFFName(ff_name): # Load FF file ff = readFF(dir_path, ff_name) # Skip the file if it is corruped if ff is None: continue total_ff_files += 1 maxpixel = ff.maxpixel avepixel = ff.avepixel # Dinterlace the images if deinterlace: maxpixel = deinterlaceBlend(maxpixel) avepixel = deinterlaceBlend(avepixel) # If the flat was given, apply it to the image, only if no subtraction is done if (flat is not None) and not subavg: maxpixel = applyFlat(maxpixel, flat) avepixel = applyFlat(avepixel, flat) # Reject the image if the median subtracted image is too bright. This usually means that there # are clouds on the image which can ruin the stack if filter_bright: img = maxpixel - avepixel # Compute surface brightness median = np.median(img) # Compute top detection pixels top_brightness = np.percentile(img, 99.9) # Reject all images where the median brightness is high # Preserve images with very bright detections if (median > 10) and (top_brightness < (2**(8 * img.itemsize) - 10)): print('Skipping: ', ff_name, 'median:', median, 'top brightness:', top_brightness) continue # Subtract the average from maxpixel if subavg: img = maxpixel - avepixel else: img = maxpixel if first_img: merge_img = np.copy(img) first_img = False continue print('Stacking: ', ff_name) # Blend images 'if lighter' merge_img = blendLighten(merge_img, img) n_stacked += 1 # If the number of stacked image is less than 20% of the given images, stack without filtering if filter_bright and (n_stacked < 0.2 * total_ff_files): return stackFFs(dir_path, file_format, deinterlace=deinterlace, subavg=subavg, filter_bright=False, flat_path=flat_path, file_list=file_list) # If no images were stacked, do nothing if n_stacked == 0: return None, None # Extract the name of the night directory which contains the FF files night_dir = os.path.basename(dir_path) stack_path = os.path.join( dir_path, night_dir + '_stack_{:d}_meteors.'.format(n_stacked) + file_format) print("Saving stack to:", stack_path) # Stretch the levels merge_img = adjustLevels(merge_img, np.percentile(merge_img, 0.5), 1.3, np.percentile(merge_img, 99.9)) # Apply the mask, if given if mask is not None: merge_img = MaskImage.applyMask(merge_img, mask) # Save the blended image saveImage(stack_path, merge_img) return stack_path, merge_img
def archiveDetections(captured_path, archived_path, ff_detected, config, extra_files=None): """ Create thumbnails and compress all files with detections and the accompanying files in one archive. Arguments: captured_path: [str] Path where the captured files are located. archived_path: [str] Path where the detected files will be archived to. ff_detected: [str] A list of FF files with detections. config: [conf object] Configuration. Keyword arguments: extra_files: [list] A list of extra files (with fill paths) which will be be saved to the night archive. Return: archive_name: [str] Name of the archive where the files were compressed to. """ # Get the list of files to archive file_list = selectFiles(config, captured_path, ff_detected) log.info('Generating thumbnails...') try: # Generate captured thumbnails captured_mosaic_file = generateThumbnails(captured_path, config, 'CAPTURED') # Generate detected thumbnails detected_mosaic_file = generateThumbnails(captured_path, config, 'DETECTED', \ file_list=sorted(file_list), no_stack=True) # Add the detected mosaic file to the selected list file_list.append(captured_mosaic_file) file_list.append(detected_mosaic_file) except Exception as e: log.error('Generating thumbnails failed with error:' + repr(e)) log.error("".join(traceback.format_exception(*sys.exc_info()))) log.info('Generating a stack of detections...') try: # Load the mask for stack mask = None if os.path.exists(config.mask_file) and config.stack_mask: mask_path = os.path.abspath(config.mask_file) mask = MaskImage.loadMask(mask_path) # Make a co-added image of all detection. Filter out possible clouds stack_path, _ = stackFFs(captured_path, 'jpg', deinterlace=(config.deinterlace_order > 0), subavg=True, \ filter_bright=True, file_list=sorted(file_list), mask=mask) if stack_path is not None: log.info("Stack saved to: {:s}".format(stack_path)) # Extract the name of the stack image stack_file = os.path.basename(stack_path) # Add the stack path to the list of files to put in the archive file_list.append(stack_file) else: log.info("Stack could not be saved!") except Exception as e: log.error('Generating stack failed with error:' + repr(e)) log.error("".join(traceback.format_exception(*sys.exc_info()))) if file_list: # Create the archive ZIP in the parent directory of the archive directory archive_name = os.path.join( os.path.abspath(os.path.join(archived_path, os.pardir)), os.path.basename(captured_path) + '_detected') # Archive the files archive_name = archiveDir(captured_path, file_list, archived_path, archive_name, \ extra_files=extra_files) return archive_name return None
metavar='MASK_PATH', type=str, help= "Apply a given flat frame. If no path to the flat is given, flat.bmp from the folder will be taken." ) # Parse the command line arguments cml_args = arg_parser.parse_args() ######################### # Load the mask mask = None if cml_args.mask is not None: if os.path.exists(cml_args.mask): mask_path = os.path.abspath(cml_args.mask) print('Loading mask:', mask_path) mask = MaskImage.loadMask(mask_path) # Run stacking stack_path, merge_img = stackFFs(cml_args.dir_path[0], cml_args.file_format[0], \ deinterlace=cml_args.deinterlace, subavg=cml_args.subavg, filter_bright=cml_args.brightfilt, \ flat_path=cml_args.flat, mask=mask) if not cml_args.hideplot: # Plot the blended image plt.imshow(merge_img, cmap='gray', vmin=0, vmax=255) plt.show()
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 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 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
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
def archiveDetections(captured_path, archived_path, ff_detected, config, extra_files=None): """ Create thumbnails and compress all files with detections and the accompanying files in one archive. Arguments: captured_path: [str] Path where the captured files are located. archived_path: [str] Path where the detected files will be archived to. ff_detected: [str] A list of FF files with detections. config: [conf object] Configuration. Keyword arguments: extra_files: [list] A list of extra files (with fill paths) which will be be saved to the night archive. Return: archive_name: [str] Name of the archive where the files were compressed to. """ log.info('Generating thumbnails...') try: # Generate captured thumbnails generateThumbnails(captured_path, config, 'CAPTURED') # Get the list of files to archive file_list = selectFiles(captured_path, ff_detected) # Generate detected thumbnails mosaic_file = generateThumbnails(captured_path, config, 'DETECTED', file_list=sorted(file_list)) # Add the detected mosaic file to the selected list file_list.append(mosaic_file) except Exception as e: log.error('Generating thumbnails failed with error:' + repr(e)) log.error(*traceback.format_exception(*sys.exc_info())) log.info('Generating a stack of detections...') try: # Load the mask for stack mask = None if os.path.exists(config.mask_file) and config.stack_mask: mask_path = os.path.abspath(config.mask_file) mask = MaskImage.loadMask(mask_path) # Make a co-added image of all detection. Filter out possible clouds stack_path, _ = stackFFs(captured_path, 'jpg', deinterlace=(config.deinterlace_order > 0), subavg=True, \ filter_bright=True, file_list=sorted(file_list), mask=mask) if stack_path is not None: # Extract the name of the stack image stack_file = os.path.basename(stack_path) # Add the stack path to the list of files to put in the archive file_list.append(stack_file) except Exception as e: log.error('Generating stack failed with error:' + repr(e)) log.error(*traceback.format_exception(*sys.exc_info())) if file_list: # Create the archive ZIP in the parent directory of the archive directory archive_name = os.path.join(os.path.abspath(os.path.join(archived_path, os.pardir)), os.path.basename(captured_path) + '_detected') # Archive the files archive_name = archiveDir(captured_path, file_list, archived_path, archive_name, \ extra_files=extra_files) return archive_name return None
def extractStars(ff_dir, ff_name, config=None, max_global_intensity=150, border=10, neighborhood_size=10, intensity_threshold=5, flat_struct=None, dark=None, mask=None): """ Extracts stars on a given FF bin by searching for local maxima and applying PSF fit for star confirmation. Source of one part of the code: http://stackoverflow.com/questions/9111711/get-coordinates-of-local-maxima-in-2d-array-above-certain-value Arguments: ff_dir: [str] Path to directory where FF files are. ff_name: [str] Name of the FF file. config: [config object] configuration object (loaded from the .config file) max_global_intensity: [int] maximum mean intensity of an image before it is discared as too bright border: [int] apply a mask on the detections by removing all that are too close to the given image border (in pixels) neighborhood_size: [int] size of the neighbourhood for the maximum search (in pixels) intensity_threshold: [float] a threshold for cutting the detections which are too faint (0-255) flat_struct: [Flat struct] Structure containing the flat field. None by default. dark: [ndarray] Dark frame. None by default. mask: [ndarray] Mask image. None by default. Return: x2, y2, background, intensity, fwhm: [list of ndarrays] - x2: X axis coordinates of the star - y2: Y axis coordinates of the star - background: background intensity - intensity: intensity of the star - Gaussian Full width at half maximum (FWHM) of fitted stars """ # This will be returned if there was an error error_return = [[], [], [], [], [], []] # Load parameters from config if given if config: max_global_intensity = config.max_global_intensity border = config.border neighborhood_size = config.neighborhood_size intensity_threshold = config.intensity_threshold # Load the FF bin file ff = FFfile.read(ff_dir, ff_name) # If the FF file could not be read, skip star extraction if ff is None: return error_return # Apply the dark frame if dark is not None: ff.avepixel = Image.applyDark(ff.avepixel, dark) # Apply the flat if flat_struct is not None: ff.avepixel = Image.applyFlat(ff.avepixel, flat_struct) # Mask the FF file if mask is not None: ff = MaskImage.applyMask(ff, mask, ff_flag=True) # Calculate image mean and stddev global_mean = np.mean(ff.avepixel) # Check if the image is too bright and skip the image if global_mean > max_global_intensity: return error_return data = ff.avepixel.astype(np.float32) # Apply a mean filter to the image to reduce noise data = ndimage.filters.convolve(data, weights=np.full((2, 2), 1.0 / 4)) # Locate local maxima on the image data_max = filters.maximum_filter(data, neighborhood_size) maxima = (data == data_max) data_min = filters.minimum_filter(data, neighborhood_size) diff = ((data_max - data_min) > intensity_threshold) maxima[diff == 0] = 0 # Apply a border mask border_mask = np.ones_like(maxima) * 255 border_mask[:border, :] = 0 border_mask[-border:, :] = 0 border_mask[:, :border] = 0 border_mask[:, -border:] = 0 maxima = MaskImage.applyMask(maxima, border_mask, image=True) # Remove all detections close to the mask image if mask is not None: erosion_kernel = np.ones((5, 5), mask.img.dtype) mask_eroded = cv2.erode(mask.img, erosion_kernel, iterations=1) maxima = MaskImage.applyMask(maxima, mask_eroded, image=True) # Find and label the maxima labeled, num_objects = ndimage.label(maxima) # Skip the image if there are too many maxima to process if num_objects > config.max_stars: print('Too many candidate stars to process! {:d}/{:d}'.format( num_objects, config.max_stars)) return error_return # Find centres of mass of each labeled objects xy = np.array( ndimage.center_of_mass(data, labeled, range(1, num_objects + 1))) # Remove all detection on the border #xy = xy[np.where((xy[:, 1] > border) & (xy[:,1] < ff.ncols - border) & (xy[:,0] > border) & (xy[:,0] < ff.nrows - border))] # Unpack star coordinates y, x = np.hsplit(xy, 2) # # Plot stars before the PSF fit # plotStars(ff, x, y) # Fit a PSF to each star x2, y2, amplitude, intensity, sigma_y_fitted, sigma_x_fitted = fitPSF( ff, global_mean, x, y, config) # x2, y2, amplitude, intensity = list(x), list(y), [], [] # Skip PSF fit # # Plot stars after PSF fit filtering # plotStars(ff, x2, y2) # Compute FWHM from one dimensional sigma sigma_x_fitted = np.array(sigma_x_fitted) sigma_y_fitted = np.array(sigma_y_fitted) sigma_fitted = np.sqrt(sigma_x_fitted**2 + sigma_y_fitted**2) fwhm = 2.355 * sigma_fitted return ff_name, x2, y2, amplitude, intensity, fwhm
def stackFFs(dir_path, file_format, deinterlace=False, subavg=False, filter_bright=False, flat_path=None, file_list=None, mask=None): """ Stack FF files in the given folder. Arguments: dir_path: [str] Path to the directory with FF files. file_format: [str] Image format for the stack. E.g. jpg, png, bmp Keyword arguments: deinterlace: [bool] True if the image shoud be deinterlaced prior to stacking. False by default. subavg: [bool] Whether the average pixel image should be subtracted form the max pixel image. False by default. filter_bright: [bool] Whether images with bright backgrounds (after average subtraction) should be skipped. False by defualt. flat_path: [str] Path to the flat calibration file. None by default. Will only be used if subavg is False. file_list: [list] A list of file for stacking. False by default, in which case all FF files in the given directory will be used. mask: [MaskStructure] Mask to apply to the stack. None by default. Return: stack_path, merge_img: - stack_path: [str] Path of the save stack. - merge_img: [ndarray] Numpy array of the stacked image. """ # Load the flat if it was given flat = None if flat_path != '': # Try finding the default flat if flat_path is None: flat_path = dir_path flat_file = 'flat.bmp' else: flat_path, flat_file = os.path.split(flat_path) flat_full_path = os.path.join(flat_path, flat_file) if os.path.isfile(flat_full_path): # Load the flat flat = loadFlat(flat_path, flat_file) print('Loaded flat:', flat_full_path) first_img = True n_stacked = 0 total_ff_files = 0 merge_img = None # If the list of files was not given, take all files in the given folder if file_list is None: file_list = sorted(os.listdir(dir_path)) # List all FF files in the current dir for ff_name in file_list: if validFFName(ff_name): # Load FF file ff = readFF(dir_path, ff_name) # Skip the file if it is corruped if ff is None: continue total_ff_files += 1 maxpixel = ff.maxpixel avepixel = ff.avepixel # Dinterlace the images if deinterlace: maxpixel = deinterlaceBlend(maxpixel) avepixel = deinterlaceBlend(avepixel) # If the flat was given, apply it to the image, only if no subtraction is done if (flat is not None) and not subavg: maxpixel = applyFlat(maxpixel, flat) avepixel = applyFlat(avepixel, flat) # Reject the image if the median subtracted image is too bright. This usually means that there # are clouds on the image which can ruin the stack if filter_bright: img = maxpixel - avepixel # Compute surface brightness median = np.median(img) # Compute top detection pixels top_brightness = np.percentile(img, 99.9) # Reject all images where the median brightness is high # Preserve images with very bright detections if (median > 10) and (top_brightness < (2**(8*img.itemsize) - 10)): print('Skipping: ', ff_name, 'median:', median, 'top brightness:', top_brightness) continue # Subtract the average from maxpixel if subavg: img = maxpixel - avepixel else: img = maxpixel if first_img: merge_img = np.copy(img) first_img = False continue print('Stacking: ', ff_name) # Blend images 'if lighter' merge_img = blendLighten(merge_img, img) n_stacked += 1 # If the number of stacked image is less than 20% of the given images, stack without filtering if filter_bright and (n_stacked < 0.2*total_ff_files): return stackFFs(dir_path, file_format, deinterlace=deinterlace, subavg=subavg, filter_bright=False, flat_path=flat_path, file_list=file_list) # If no images were stacked, do nothing if n_stacked == 0: return None, None # Extract the name of the night directory which contains the FF files night_dir = os.path.basename(dir_path) stack_path = os.path.join(dir_path, night_dir + '_stack_{:d}_meteors.'.format(n_stacked) + file_format) print("Saving stack to:", stack_path) # Stretch the levels merge_img = adjustLevels(merge_img, np.percentile(merge_img, 0.5), 1.3, np.percentile(merge_img, 99.9)) # Apply the mask, if given if mask is not None: merge_img = MaskImage.applyMask(merge_img, mask) # Save the blended image scipy.misc.imsave(stack_path, merge_img) return stack_path, merge_img
arg_parser.add_argument('-m', '--mask', metavar='MASK_PATH', type=str, help="Apply a given flat frame. If no path to the flat is given, flat.bmp from the folder will be taken.") # Parse the command line arguments cml_args = arg_parser.parse_args() ######################### # Load the mask mask = None if cml_args.mask is not None: if os.path.exists(cml_args.mask): mask_path = os.path.abspath(cml_args.mask) print('Loading mask:', mask_path) mask = MaskImage.loadMask(mask_path) # Run stacking stack_path, merge_img = stackFFs(cml_args.dir_path[0], cml_args.file_format[0], \ deinterlace=cml_args.deinterlace, subavg=cml_args.subavg, filter_bright=cml_args.brightfilt, \ flat_path=cml_args.flat, mask=mask) if not cml_args.hideplot: # Plot the blended image plt.imshow(merge_img, cmap='gray', vmin=0, vmax=255) plt.show()
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