def center_of_gravity(frame): """ Comppute (y, x) pixel coordinates of the center of gravity for a given monochrome frame. Raise an error if the computed cog is outside the frame index bounds. :param frame: Monochrome frame (2D numpy array) :return: Integer pixel coordinates (center_y, center_x) of center of gravity """ # Convert the grayscale image to binary image, where all pixels # brighter than the half the maximum image brightness are set to 1, # and all others are set to 0. thresh = threshold(frame, frame.max() / 2, 1, THRESH_BINARY)[1] # Calculate moments of binary image M = moments(thresh) # Calculate coordinates for center of gravity and round pixel # coordinates to the nearest integers. cog_x = round(M["m10"] / M["m00"]) cog_y = round(M["m01"] / M["m00"]) # If the computed center of gravity is outside the frame bounds, raise an error (should be # impossible). if not 0 < cog_y < frame.shape[0] or not 0 < cog_x < frame.shape[1]: raise InternalError("Center of gravity coordinates [" + str(cog_y) + ", " + str(cog_x) + "] of reference frame are out of bounds") return cog_y, cog_x
def field(self): result = self.raw_field() for line in result: for i in range(len(line)): lst = line[i] if (len(lst)) > 1: raise InternalError("Too many objects in cell: {0}".format(lst)) if line[i]: line[i] = line[i][0] else: line[i] = None return result
def __init__(self, job_name): """ Initialize a Job object, given its name. The following instance variables are set: - name: Path name string coming from the file chooser. - file_name: File name string without path. - type: Either 'video', or 'image' for stacking jobs, or 'postproc' for postprocessing. - bayer_option_selected: Initialized to 'Auto detect color' for file types for which debayering is supported. Otherwise None. - bayer_pattern: Initialized to None :param job_name: Name of the job (str) """ self.name = job_name path = Path(self.name) self.file_name = path.name # Bayer patterns are only defined for type 'video'. self.bayer_pattern = None self.bayer_option_selected = None # Set the type of the job based on the file name extension. image_extensions = ['.tif', '.tiff', '.fit', '.fits', '.jpg', '.png'] video_extensions = ['.avi', '.mov', '.mp4', '.ser'] if path.is_file(): extension = path.suffix.lower() if extension in video_extensions: self.type = 'video' self.bayer_option_selected = 'Auto detect color' elif extension in image_extensions: self.type = 'postproc' else: raise InternalError("Unsupported file type '" + extension + "' specified for job") elif path.is_dir(): self.type = 'image' else: raise InternalError( "Cannot decide if input file is video or image directory")
def accept(self): """ If the OK button is clicked and the job list has been changed update the job list in the parent object. :return: - """ image_extensions = ['.tif', '.tiff', '.fit', '.fits', '.jpg', '.png'] video_extensions = ['.avi', '.ser'] # Set the job types of all current jobs on the list. self.job_types = [] for job in self.job_names: if Path(job).is_file(): extension = Path(job).suffix.lower() if extension in video_extensions: self.job_types.append('video') elif extension in image_extensions: self.job_types.append('postproc') else: raise InternalError("Unsupported file type '" + extension + "' specified for job") elif Path(job).is_dir(): self.job_types.append('image') else: raise InternalError( "Cannot decide if input file is video or image directory") # Update the job list and reset the current job index to the first entry. self.parent_gui.job_names = self.job_names self.parent_gui.job_types = self.job_types self.parent_gui.job_number = len(self.job_names) self.parent_gui.job_index = 0 self.parent_gui.activity = "Read frames" self.parent_gui.activate_gui_elements( [self.parent_gui.ui.box_automatic], True) self.parent_gui.update_status() self.close()
def center_of_gravity(frame): """ Comppute (y, x) pixel coordinates of the center of gravity for a given monochrome frame. Raise an error if the computed cog is outside the frame index bounds. :param frame: Monochrome frame (2D numpy array) :return: Integer pixel coordinates (center_y, center_x) of center of gravity """ # The following is the old algorithm (up to Version 0.8.5). It does not work well for # planets on a bright sky background. # # Convert the grayscale image to binary image, where all pixels # brighter than half the maximum image brightness are set to 1, # and all others are set to 0. # thresh = threshold(frame, frame.max()/2, 1, THRESH_BINARY)[1] # This new code sets the threshold between the minimal brightness (background) and # the maximal brightness (object). The hope is that this way the threshold is far away from # background noise. Also, no binary image is created, but brightness variations are allowed # to influence the center of gravity. This gives brighter parts of the image more weight, # which results in a slightly better precision. minVal, maxVal, minLoc, maxLoc = minMaxLoc(frame) brightness_threshold = int((minVal+maxVal)/2) thresh = clip(frame, brightness_threshold, None)[:,:]-brightness_threshold # Calculate moments of binary image M = moments(thresh) # Calculate coordinates for center of gravity and round pixel # coordinates to the nearest integers. cog_x = round(M["m10"] / M["m00"]) cog_y = round(M["m01"] / M["m00"]) # If the computed center of gravity is outside the frame bounds, raise an error (should be # impossible). if not 0 < cog_y < frame.shape[0] or not 0 < cog_x < frame.shape[1]: raise InternalError( "Center of gravity coordinates [" + str(cog_y) + ", " + str( cog_x) + "] of reference frame are out of bounds") return cog_y, cog_x
def best_frame_indices_in_empty_areas(self, index_y, index_x): """ For a quality area without any alignment point, find the closest quality area with alignment points and return its list of frame indices ranked by the local frame quality in decreasing order. :param index_y: y coordinate of the quality area in the rectangular grid of quality areas :param index_x: x coordinate of the quality area in the rectangular grid of quality areas :return: frame index list, ranked by the image quality at the closest quality area with alignment points """ # Go though circles with increasing radius "distance" around the current quality area. for distance in arange(1, max(self.y_dim, self.x_dim)): circle = Miscellaneous.circle_around(index_x, index_y, distance) for (compare_x, compare_y) in circle: # If the coordinates are within the quality area grid, and if the area at this # location has a non-empty list of alignment points, return its list. if 0 <= compare_x < self.x_dim and 0 <= compare_y < self.y_dim and \ self.quality_areas[compare_y][compare_x]['alignment_point_indices']: return self.quality_areas[compare_y][compare_x]['best_frame_indices'] # This should never happen, because it means that there is not any quality area with an # alignment point. raise InternalError("No quality area contains any alignment point")
def align_frames(self): """ Compute the displacement of all frames relative to the sharpest frame using the alignment rectangle. :return: - """ if self.configuration.align_frames_mode == "Surface": # For "Surface" mode the alignment rectangle has to be selected first. if self.x_low_opt is None: raise WrongOrderingError( "Method 'align_frames' is called before 'select_alignment_rect'" ) # From the sharpest frame cut out the alignment rectangle. The shifts of all other frames # will be computed relativ to this patch. if self.configuration.align_frames_method == "MultiLevelCorrelation": # MultiLevelCorrelation uses two reference windows with different resolution. Also, # please note that the data type is float32 in this case. reference_frame = self.frames.frames_mono_blurred( self.frame_ranks_max_index).astype(float32) self.reference_window = reference_frame[ self.y_low_opt:self.y_high_opt, self.x_low_opt:self.x_high_opt] # For the first phase a box with half the resolution is constructed. self.reference_window_first_phase = self.reference_window[:: 2, :: 2] else: # For all other methods, the reference window is of type int32. reference_frame = self.frames.frames_mono_blurred( self.frame_ranks_max_index).astype(int32) self.reference_window = reference_frame[ self.y_low_opt:self.y_high_opt, self.x_low_opt:self.x_high_opt] self.reference_window_shape = self.reference_window.shape elif self.configuration.align_frames_mode == "Planet": # For "Planetary" mode compute the center of gravity for the reference image. cog_reference_y, cog_reference_x = AlignFrames.center_of_gravity( self.frames.frames_mono_blurred(self.frame_ranks_max_index)) else: raise NotSupportedError("Frame alignment mode '" + self.configuration.align_frames_mode + "' not supported") # Initialize a list which for each frame contains the shifts in y and x directions. self.frame_shifts = [None] * self.frames.number # Initialize a counter of processed frames for progress bar signalling. It is set to one # because in the loop below the optimal frame is not counted. number_processed = 1 # Loop over all frames. Begin with the sharpest (reference) frame for idx in chain(reversed(range(self.frame_ranks_max_index + 1)), range(self.frame_ranks_max_index, self.frames.number)): if idx == self.frame_ranks_max_index: # For the sharpest frame the displacement is 0 because it is used as the reference. self.frame_shifts[idx] = [0, 0] # Initialize two variables which keep the shift values of the previous step as # the starting point for the next step. This reduces the search radius if frames are # drifting. dy_min_cum = dx_min_cum = 0 # For all other frames: Compute the global shift, using the "blurred" monochrome image. else: # After every "signal_step_size"th frame, send a progress signal to the main GUI. if self.progress_signal is not None and number_processed % self.signal_step_size == 1: self.progress_signal.emit( "Align all frames", int( round(10 * number_processed / self.frames.number) * 10)) frame = self.frames.frames_mono_blurred(idx) # In Planetary mode the shift of the "center of gravity" of the image is computed. # This algorithm cannot fail. if self.configuration.align_frames_mode == "Planet": cog_frame = AlignFrames.center_of_gravity(frame) self.frame_shifts[idx] = [ cog_reference_y - cog_frame[0], cog_reference_x - cog_frame[1] ] # In Surface mode various methods can be used to measure the shift from one frame # to the next. The method "Translation" is special: Using phase correlation it is # the only method not based on a local search algorithm. It is treated differently # here because it does not require a re-shifting of the alignment patch. elif self.configuration.align_frames_method == "Translation": # The shift is computed with cross-correlation. Cut out the alignment patch and # compute its translation relative to the reference. frame_window = self.frames.frames_mono_blurred( idx)[self.y_low_opt:self.y_high_opt, self.x_low_opt:self.x_high_opt] self.frame_shifts[idx] = Miscellaneous.translation( self.reference_window, frame_window, self.reference_window_shape) # Now treat all "Surface" mode cases using local search algorithms. In each case # the result is the shift vector [dy_min, dx_min]. The search can fail (if within # the search radius no optimum is found). If that happens for at least one frame, # an exception is raised. The workflow thread then tries again using another # alignment patch. else: if self.configuration.align_frames_method == "MultiLevelCorrelation": # The shift is computed in two phases: First on a coarse pixel grid, # and then on the original grid in a small neighborhood around the optimum # found in the first phase. shift_y_local_first_phase, shift_x_local_first_phase, \ success_first_phase, shift_y_local_second_phase, \ shift_x_local_second_phase, success_second_phase = \ Miscellaneous.multilevel_correlation( self.reference_window_first_phase, frame, self.configuration.frames_gauss_width, self.reference_window, self.y_low_opt - dy_min_cum, self.y_high_opt - dy_min_cum, self.x_low_opt - dx_min_cum, self.x_high_opt - dx_min_cum, self.configuration.align_frames_search_width, weight_matrix_first_phase=None) success = success_first_phase and success_second_phase if success: [dy_min, dx_min] = [ shift_y_local_first_phase + shift_y_local_second_phase, shift_x_local_first_phase + shift_x_local_second_phase ] elif self.configuration.align_frames_method == "RadialSearch": # Spiral out from the shift position of the previous frame and search for the # local optimum. [dy_min, dx_min], dev_r = Miscellaneous.search_local_match( self.reference_window, frame, self.y_low_opt - dy_min_cum, self.y_high_opt - dy_min_cum, self.x_low_opt - dx_min_cum, self.x_high_opt - dx_min_cum, self.configuration.align_frames_search_width, self.configuration.align_frames_sampling_stride, sub_pixel=False) # The search was not successful if a zero shift was reported after more # than two search cycles. success = len(dev_r) <= 2 or dy_min != 0 or dx_min != 0 elif self.configuration.align_frames_method == "SteepestDescent": # Spiral out from the shift position of the previous frame and search for the # local optimum. [dy_min, dx_min ], dev_r = Miscellaneous.search_local_match_gradient( self.reference_window, frame, self.y_low_opt - dy_min_cum, self.y_high_opt - dy_min_cum, self.x_low_opt - dx_min_cum, self.x_high_opt - dx_min_cum, self.configuration.align_frames_search_width, self.configuration.align_frames_sampling_stride, self.dev_table) # The search was not successful if a zero shift was reported after more # than two search cycles. success = len(dev_r) <= 2 or dy_min != 0 or dx_min != 0 else: raise NotSupportedError( "Frame alignment method " + configuration.align_frames_method + " not supported") # If the local search was unsuccessful, quit the frame loop with an error. if not success: raise InternalError("frame " + str(idx)) # Update the cumulative shift values to be used as starting point for the # next frame. dy_min_cum += dy_min dx_min_cum += dx_min self.frame_shifts[idx] = [dy_min_cum, dx_min_cum] # If the alignment window gets too close to a frame edge, move it away from # that edge by half the border width. First check if the reference window still # fits into the shifted frame. if self.shape[0] - abs( dy_min_cum) - 2 * self.configuration.align_frames_search_width - \ self.configuration.align_frames_border_width < \ self.reference_window_shape[0] or self.shape[1] - abs( dx_min_cum) - 2 * self.configuration.align_frames_search_width - \ self.configuration.align_frames_border_width < \ self.reference_window_shape[1]: raise ArgumentError( "Frame stabilization window does not fit into" " intersection") new_reference_window = False # Start with the lower y edge. while self.y_low_opt - dy_min_cum < \ self.configuration.align_frames_search_width + \ self.configuration.align_frames_border_width / 2: self.y_low_opt += ceil( self.configuration.align_frames_border_width / 2.) self.y_high_opt += ceil( self.configuration.align_frames_border_width / 2.) new_reference_window = True # Now the upper y edge. while self.y_high_opt - dy_min_cum > self.shape[ 0] - self.configuration.align_frames_search_width - \ self.configuration.align_frames_border_width / 2: self.y_low_opt -= ceil( self.configuration.align_frames_border_width / 2.) self.y_high_opt -= ceil( self.configuration.align_frames_border_width / 2.) new_reference_window = True # Now the lower x edge. while self.x_low_opt - dx_min_cum < \ self.configuration.align_frames_search_width + \ self.configuration.align_frames_border_width / 2: self.x_low_opt += ceil( self.configuration.align_frames_border_width / 2.) self.x_high_opt += ceil( self.configuration.align_frames_border_width / 2.) new_reference_window = True # Now the upper x edge. while self.x_high_opt - dx_min_cum > self.shape[ 1] - self.configuration.align_frames_search_width - \ self.configuration.align_frames_border_width / 2: self.x_low_opt -= ceil( self.configuration.align_frames_border_width / 2.) self.x_high_opt -= ceil( self.configuration.align_frames_border_width / 2.) new_reference_window = True # If the window was moved, update the "reference window(s)". if new_reference_window: if self.configuration.align_frames_method == "MultiLevelCorrelation": self.reference_window = reference_frame[ self.y_low_opt:self.y_high_opt, self.x_low_opt:self.x_high_opt] # For the first phase a box with half the resolution is constructed. self.reference_window_first_phase = self.reference_window[:: 2, :: 2] else: self.reference_window = reference_frame[ self.y_low_opt:self.y_high_opt, self.x_low_opt:self.x_high_opt] # This frame is processed, go to next one. number_processed += 1 if self.progress_signal is not None: self.progress_signal.emit("Align all frames", 100) # Compute the shape of the area contained in all frames in the form [[y_low, y_high], # [x_low, x_high]] self.intersection_shape = [[ max(b[0] for b in self.frame_shifts), min(b[0] for b in self.frame_shifts) + self.shape[0] ], [ max(b[1] for b in self.frame_shifts), min(b[1] for b in self.frame_shifts) + self.shape[1] ]]
def align_frames(self): """ Compute the displacement of all frames relative to the sharpest frame using the alignment rectangle. :return: - """ if self.configuration.align_frames_mode == "Surface": # For "Surface" mode the alignment rectangle has to be selected first. if self.x_low_opt is None: raise WrongOrderingError( "Method 'align_frames' is called before 'select_alignment_rect'" ) # From the sharpest frame cut out the alignment rectangle. The shifts of all other frames # will be computed relativ to this patch. self.reference_window = self.frames.frames_mono_blurred( self.frame_ranks_max_index)[ self.y_low_opt:self.y_high_opt, self.x_low_opt:self.x_high_opt].astype(int32) self.reference_window_shape = self.reference_window.shape elif self.configuration.align_frames_mode == "Planet": # For "Planetary" mode compute the center of gravity for the reference image. cog_reference_y, cog_reference_x = AlignFrames.center_of_gravity( self.frames.frames_mono_blurred(self.frame_ranks_max_index)) else: raise NotSupportedError("Frame alignment mode '" + self.configuration.align_frames_mode + "' not supported") # Initialize a list which for each frame contains the shifts in y and x directions. self.frame_shifts = [None] * self.frames.number # Initialize lists with info on failed frames. self.dev_r_list = [] self.failed_index_list = [] # Initialize a counter of processed frames for progress bar signalling. It is set to one # because in the loop below the optimal frame is not counted. number_processed = 1 # Loop over all frames. Begin with the sharpest (reference) frame for idx in chain(reversed(range(self.frame_ranks_max_index + 1)), range(self.frame_ranks_max_index, self.frames.number)): if idx == self.frame_ranks_max_index: # For the sharpest frame the displacement is 0 because it is used as the reference. self.frame_shifts[idx] = [0, 0] # Initialize two variables which keep the shift values of the previous step as # the starting point for the next step. This reduces the search radius if frames are # drifting. dy_min_cum = dx_min_cum = 0 # For all other frames: Compute the global shift, using the "blurred" monochrome image. else: # After every "signal_step_size"th frame, send a progress signal to the main GUI. if self.progress_signal is not None and number_processed % self.signal_step_size == 1: self.progress_signal.emit( "Align all frames", int((number_processed / self.frames.number) * 100.)) frame = self.frames.frames_mono_blurred(idx) if self.configuration.align_frames_mode == "Planet": # In Planetary mode the shift of the "center of gravity" of the image is # computed. This algorithm cannot fail. cog_frame = AlignFrames.center_of_gravity(frame) self.frame_shifts[idx] = [ cog_reference_y - cog_frame[0], cog_reference_x - cog_frame[1] ] number_processed += 1 continue # In "Surface" mode three alignment algorithms can be chosen from. In each case # the result is the shift vector [dy_min, dx_min]. The second and third algorithm # do a local search. It can fail if within the search radius no minimum is found. # The first algorithm (cross-correlation) can fail as well, but in this case there # is no indication that this happened. elif self.configuration.align_frames_method == "Translation": # The shift is computed with cross-correlation. Cut out the alignment patch and # compute its translation relative to the reference. frame_window = self.frames.frames_mono_blurred( idx)[self.y_low_opt:self.y_high_opt, self.x_low_opt:self.x_high_opt] self.frame_shifts[idx] = Miscellaneous.translation( self.reference_window, frame_window, self.reference_window_shape) continue elif self.configuration.align_frames_method == "RadialSearch": # Spiral out from the shift position of the previous frame and search for the # local optimum. [dy_min, dx_min], dev_r = Miscellaneous.search_local_match( self.reference_window, frame, self.y_low_opt - dy_min_cum, self.y_high_opt - dy_min_cum, self.x_low_opt - dx_min_cum, self.x_high_opt - dx_min_cum, self.configuration.align_frames_search_width, self.configuration.align_frames_sampling_stride, sub_pixel=False) elif self.configuration.align_frames_method == "SteepestDescent": # Spiral out from the shift position of the previous frame and search for the # local optimum. [dy_min, dx_min ], dev_r = Miscellaneous.search_local_match_gradient( self.reference_window, frame, self.y_low_opt - dy_min_cum, self.y_high_opt - dy_min_cum, self.x_low_opt - dx_min_cum, self.x_high_opt - dx_min_cum, self.configuration.align_frames_search_width, self.configuration.align_frames_sampling_stride, self.dev_table) else: raise NotSupportedError("Frame alignment method " + configuration.align_frames_method + " not supported") # Update the cumulative shift values to be used as starting point for the # next frame. dy_min_cum += dy_min dx_min_cum += dx_min self.frame_shifts[idx] = [dy_min_cum, dx_min_cum] # In "Surface" mode shift computation can fail if no minimum is found within # the pre-defined search radius. if len(dev_r) > 2 and dy_min == 0 and dx_min == 0: self.failed_index_list.append(idx) self.dev_r_list.append(dev_r) continue # If the alignment window gets too close to a frame edge, move it away from # that edge by half the border width. First check if the reference window still # fits into the shifted frame. if self.shape[0] - abs( dy_min_cum) - 2 * self.configuration.align_frames_search_width - \ self.configuration.align_frames_border_width < \ self.reference_window_shape[0] or self.shape[1] - abs( dx_min_cum) - 2 * self.configuration.align_frames_search_width - \ self.configuration.align_frames_border_width < \ self.reference_window_shape[1]: raise ArgumentError( "Frame stabilization window does not fit into" " intersection") new_reference_window = False # Start with the lower y edge. while self.y_low_opt - dy_min_cum < \ self.configuration.align_frames_search_width + \ self.configuration.align_frames_border_width / 2: self.y_low_opt += ceil( self.configuration.align_frames_border_width / 2.) self.y_high_opt += ceil( self.configuration.align_frames_border_width / 2.) new_reference_window = True # Now the upper y edge. while self.y_high_opt - dy_min_cum > self.shape[ 0] - self.configuration.align_frames_search_width - \ self.configuration.align_frames_border_width / 2: self.y_low_opt -= ceil( self.configuration.align_frames_border_width / 2.) self.y_high_opt -= ceil( self.configuration.align_frames_border_width / 2.) new_reference_window = True # Now the lower x edge. while self.x_low_opt - dx_min_cum < \ self.configuration.align_frames_search_width + \ self.configuration.align_frames_border_width / 2: self.x_low_opt += ceil( self.configuration.align_frames_border_width / 2.) self.x_high_opt += ceil( self.configuration.align_frames_border_width / 2.) new_reference_window = True # Now the upper x edge. while self.x_high_opt - dx_min_cum > self.shape[ 1] - self.configuration.align_frames_search_width - \ self.configuration.align_frames_border_width / 2: self.x_low_opt -= ceil( self.configuration.align_frames_border_width / 2.) self.x_high_opt -= ceil( self.configuration.align_frames_border_width / 2.) new_reference_window = True # If the window was moved, update the "reference_window". if new_reference_window: self.reference_window = self.frames.frames_mono_blurred( self.frame_ranks_max_index)[ self.y_low_opt:self.y_high_opt, self.x_low_opt:self.x_high_opt].astype(int32) number_processed += 1 if self.progress_signal is not None: self.progress_signal.emit("Align all frames", 100) # Compute the shape of the area contained in all frames in the form [[y_low, y_high], # [x_low, x_high]] self.intersection_shape = [[ max(b[0] for b in self.frame_shifts), min(b[0] for b in self.frame_shifts) + self.shape[0] ], [ max(b[1] for b in self.frame_shifts), min(b[1] for b in self.frame_shifts) + self.shape[1] ]] if len(self.failed_index_list) > 0: raise InternalError("No valid shift computed for " + str(len(self.failed_index_list)) + " frames: " + str(self.failed_index_list))