def initialize_auto_align(self, camera_socket): """ Establish the relation between the directions of (x,y) coordinates in an idealized pixel image of the Moon (x positive to the east, y positive southwards) and the (x,y) coordinates of the normalized plane in which the tile construction is done (x positive to the right, y positive upwards). Take into account potential mirror inversion in the optical system. :param camera_socket: interface to the camera to capture videos and still images :return: fraction of alignment error as compared to width of overlap between tiles """ self.autoalign_initialized = False try: # Capture an alignment reference frame self.im_shift = ImageShift(self.configuration, camera_socket, debug=self.debug) except RuntimeError: if self.configuration.protocol_level > 0: Miscellaneous.protocol( "Auto-alignment initialization failed in capturing alignment reference frame.") raise RuntimeError if self.configuration.protocol_level > 1: Miscellaneous.protocol("Alignment reference frame captured.") # The shift_angle is the overlap width between panorama tiles (in radians). self.shift_angle = self.im_shift.ol_angle # Three positions in the sky are defined: right shift in x direction, zero shift, and # downward shift in y direction. (x,y) are the pixel coordinates in the still images # captured with the video camera. All shifts are relative to the current coordinates of # the landmark. shift_vectors = [[self.shift_angle, 0.], [0., 0.], [0., self.shift_angle]] xy_shifts = [] for shift in shift_vectors: # Compute current coordinates of landmark, including corrections for alignment and drift (ra_landmark, de_landmark) = (self.compute_telescope_coordinates_of_landmark()) # Transform (x,y) coordinates into (ra,de) coordinates. The y-flip has to be set to -1. # because the rotate function assumes the y coordinate to point up, whereas the y pixel # coordinate is pointing down (see comment in method align. (shift_angle_ra, shift_angle_de) = Miscellaneous.rotate(self.me.pos_angle_pole, self.me.de, 1., 1., -1., shift[0], shift[1]) # Drive the telescope to the computed position in the sky. self.tel.slew_to(ra_landmark + shift_angle_ra, de_landmark + shift_angle_de) # Wait until the telescope orientation has stabilized. time.sleep(self.configuration.conf.getfloat("ASCOM", "wait interval")) try: # Capture a still image of the area around landmark and determine the shift versus # the reference frame. (x_shift, y_shift, in_cluster, outliers) = self.im_shift.shift_vs_reference() # If the image was not good enough for automatic shift determination, disable auto- # alignment. except RuntimeError as e: if self.configuration.protocol_level > 2: Miscellaneous.protocol(str(e)) raise RuntimeError if self.configuration.protocol_level > 2: Miscellaneous.protocol("Frame captured for auto-alignment, x_shift: " + str( round(x_shift / self.im_shift.pixel_angle, 1)) + ", y_shift: " + str( round(y_shift / self.im_shift.pixel_angle, 1)) + " (pixels), # consistent shifts: " + str( in_cluster) + ", # outliers: " + str(outliers) + ".") xy_shifts.append([x_shift, y_shift]) # Subtract second position from first and third position and reverse the vector. Reason for # the reversal: The shift has been applied to the mount pointing. The shift measured in the # image is the opposite of the mount shift. shift_vector_0_measured = [xy_shifts[1][0] - xy_shifts[0][0], xy_shifts[1][1] - xy_shifts[0][1]] shift_vector_2_measured = [xy_shifts[1][0] - xy_shifts[2][0], xy_shifts[1][1] - xy_shifts[2][1]] # Compare measured shifts in x and y with the expected directions to find out if images # are mirror-inverted in x or y. self.flip_x = np.sign(shift_vector_0_measured[0]) self.flip_y = np.sign(shift_vector_2_measured[1]) if self.configuration.protocol_level > 2: if self.flip_x < 0: Miscellaneous.protocol("Auto-alignment, image flipped horizontally.") else: Miscellaneous.protocol("Auto-alignment, image not flipped horizontally.") if self.flip_y < 0: Miscellaneous.protocol("Auto-alignment, image flipped vertically.") else: Miscellaneous.protocol("Auto-alignment, image not flipped vertically.") # Determine how much the measured shifts deviate from the expected shifts in the focal # plane. If the difference is too large, auto-alignment initialization is interpreted as # not successful. error_x = abs(abs(shift_vector_0_measured[0]) - self.shift_angle) / self.shift_angle error_y = abs(abs(shift_vector_2_measured[1]) - self.shift_angle) / self.shift_angle error = max(error_x, error_y) focal_length_x = abs( shift_vector_0_measured[0]) / self.shift_angle * self.im_shift.focal_length focal_length_y = abs( shift_vector_2_measured[1]) / self.shift_angle * self.im_shift.focal_length if self.configuration.protocol_level > 1: Miscellaneous.protocol("Focal length measured in x direction: " + str( round(focal_length_x, 1)) + ", in y direction: " + str( round(focal_length_y, 1)) + " (mm).") if error > self.configuration.align_max_autoalign_error: if self.configuration.protocol_level > 0: Miscellaneous.protocol( "Auto-alignment initialization failed, focal length error in x: " + str( round(error_x * 100., 1)) + ", in y: " + str( round(error_y * 100., 1)) + " (percent).") raise RuntimeError else: if self.configuration.protocol_level > 0: Miscellaneous.protocol("Auto-alignment successful, focal length error in x: " + str( round(error_x * 100., 1)) + ", in y: " + str( round(error_y * 100., 1)) + " (percent).") self.autoalign_initialized = True # Return the relative error as compared with tile overlap width. return error
def align(self, alignment_manual=True): """ Determine the current error in telescope pointing, either with the help of the user (manual mode) or automatically (auto-alignment). :param alignment_manual: True if the telescope has been aimed at landmark by the user :return: In case alignment_manual=False (auto-alignment), return the relative alignment error. The deviation of the current positioning as compared to the expected position, based on the previous alignment, is determined. The quotient of this deviation and the width of the overlap between tiles is returned. If it is too large, a complete panorama coverage cannot be guaranteed. In case of manual_alignment=True, return None. """ # Alignment is only possible after a landmark has been selected. if not self.landmark_offset_set: if self.configuration.protocol_level > 0: Miscellaneous.protocol("Error in alignment: Landmark offset not set.") raise RuntimeError("Error: Landmark offset not set.") # Manual alignment: The telescope is aimed at the current location of the landmark. Look # up its position and proceed to alignment computation. if alignment_manual: # The telescope position is delivered by the mount driver (ra_landmark, de_landmark) = self.tel.lookup_tel_position() relative_alignment_error = None # Auto-alignment: No assumption on the current telescope pointing can be made. else: # Automatic alignment: check if auto-alignment has been initialized if not self.autoalign_initialized: raise RuntimeError("Error: Attempt to do an auto-alignment before initialization.") # Move telescope to expected coordinates of alignment point (ra_landmark, de_landmark) = (self.compute_telescope_coordinates_of_landmark()) self.tel.slew_to(ra_landmark, de_landmark) time.sleep(self.configuration.conf.getfloat("ASCOM", "wait interval")) try: # Measure shift against reference frame (x_shift, y_shift, in_cluster, outliers) = self.im_shift.shift_vs_reference() if self.configuration.protocol_level > 1: Miscellaneous.protocol("New alignment frame analyzed, x_shift: " + str( round(x_shift / self.im_shift.pixel_angle, 1)) + ", y_shift: " + str( round(y_shift / self.im_shift.pixel_angle, 1)) + " (pixels), # consistent shifts: " + str( in_cluster) + ", # outliers: " + str(outliers) + ".") except RuntimeError as e: if self.configuration.protocol_level > 0: Miscellaneous.protocol("Exception in auto-alignment: " + str(e)) raise RuntimeError(str(e)) global_shift = sqrt(x_shift ** 2 + y_shift ** 2) relative_alignment_error = global_shift / self.shift_angle # Translate shifts measured in camera image into equatorial coordinates scale_factor = 1. # In tile construction (where the rotate function had been designed for) x is pointing # right and y upwards. Here, x is pointing right and y downwards. Therefore, the y flip # has to be reversed. (ra_shift, de_shift) = Miscellaneous.rotate(self.me.pos_angle_pole, self.me.de, scale_factor, self.flip_x, -1. * self.flip_y, x_shift, y_shift) if self.configuration.protocol_level > 2: Miscellaneous.protocol("Alignment shift rotated to RA/DE: RA: " + str( round(ra_shift / self.im_shift.pixel_angle, 1)) + ", DE: " + str( round(de_shift / self.im_shift.pixel_angle, 1)) + " (pixels).") # The shift is computed as "current frame - reference". Add coordinate shifts to current # mount position to get mount setting where landmark is located as on reference frame. ra_landmark += ra_shift de_landmark += de_shift # From here on, manual and auto-alignment can be treated the same. The current mount # position is given by(ra_landmark, de_landmark). current_time = datetime.now() # Set the time of the alignment point with an accuracy better than a second. self.alignment_time = self.current_time_seconds(current_time) # Update ephemeris of moon and sun self.me.update(current_time) # Correction = telescope position minus updated ephemeris position of # landmark self.ra_correction = ra_landmark - (self.me.ra + self.ra_offset_landmark) self.de_correction = de_landmark - (self.me.de + self.de_offset_landmark) if self.configuration.protocol_level > 0: Miscellaneous.protocol("Computing new alignment, current RA correction ('): " + str( round(degrees(self.ra_correction) * 60., 3)) + ", current DE correction ('): " + str( round(degrees(self.de_correction) * 60., 3)) + ".") if self.configuration.protocol_level > 2: Miscellaneous.protocol("More alignment info: moon center RA: " + str( round(degrees(self.me.ra), 5)) + ", moon center DE: " + str( round(degrees(self.me.de), 5)) + ", landmark RA: " + str( round(degrees(ra_landmark), 5)) + ", landmark DE: " + str( round(degrees(de_landmark), 5)) + " (all in degrees).") # Store a new alignment point alignment_point = {} alignment_point['time_string'] = str(current_time)[11:19] alignment_point['time_seconds'] = self.alignment_time alignment_point['ra_correction'] = self.ra_correction alignment_point['de_correction'] = self.de_correction self.alignment_points.append(alignment_point) self.is_aligned = True # If more than one alignment point is stored, enable drift dialog and compute drift rate # of telescope mount. if len(self.alignment_points) > 1: self.drift_dialog_enabled = True if self.default_last_drift: self.last_index = len(self.alignment_points) - 1 self.compute_drift_rate() return relative_alignment_error
def __init__(self, configuration, de_center, m_diameter, phase_angle, pos_angle): """ Read out parameters from the configuration object and compute the optimal tile coverage. :param configuration: object containing parameters set by the user :param de_center: declination of the moon's center (radians) :param m_diameter: diameter of the moon (radians) :param phase_angle: phase angle of the sunlit moon phase (0. for New Moon, Pi for Full Moon) :param pos_angle: angle (radians) between North and the "North Pole" of the sunlit phase, counted counterclockwise """ # Allocate instance variables used in Tile Visualization later. self.m_diameter = m_diameter self.phase_angle = phase_angle # Configuration data pixel_size = (configuration.conf.getfloat("Camera", "pixel size")) focal_length = (configuration.conf.getfloat("Telescope", "focal length")) im_h_pixel = configuration.conf.getint("Camera", "pixel vertical") im_w_pixel = (configuration.conf.getint("Camera", "pixel horizontal")) ol_outer_pixel = (configuration.conf.getint("Camera", "external margin pixel")) ol_inner_min_pixel = (configuration.conf.getint( "Camera", "tile overlap pixel")) self.limb_first = (configuration.conf.getboolean( "Workflow", "limb first")) # Height / width of the image, external margin width and tile overlap in radians self.im_h = float(im_h_pixel) * atan(pixel_size / focal_length) self.im_w = float(im_w_pixel) * atan(pixel_size / focal_length) self.ol_outer = float(ol_outer_pixel) * atan(pixel_size / focal_length) ol_inner_min = float(ol_inner_min_pixel) * atan( pixel_size / focal_length) # Auxiliary parameters used by the "rotate" method in module miscellaneous flip_x = 1. flip_y = 1. scale_factor = 1. # Compute the minimum number of tile rows needed to fulfill overlap requirements. n_rows = (m_diameter + 2. * self.ol_outer - ol_inner_min) / (self.im_h - ol_inner_min) # Round up to next integer. n_rows_corrected = int(ceil(n_rows)) # Increase the vertical tile overlap such that the external margin width is as specified. if n_rows_corrected > 1: self.ol_inner_v = (n_rows_corrected * self.im_h - m_diameter - 2. * self.ol_outer) / (n_rows_corrected - 1.) # Only one row of tiles (very unlikely though) else: self.ol_inner_v = ol_inner_min # Initialize the tile structure: All tiles in one row form a list. These lists are collected # in "lists_of_tiles". self.lists_of_tiles = [] # Initialize the maximum number of tiles in a row. max_cols = 0 # Construct each row of tiles. The origin of the (x,y) coordinate system is at the moon # center, x pointing right, y up. x and y are in radians. m_radius = m_diameter / 2. for i in range(n_rows_corrected): # Compute the y coordinates for the top and bottom of the row. y_top = m_radius + self.ol_outer - i * (self.im_h - self.ol_inner_v) y_bottom = y_top - self.im_h # Compute the x coordinates where the moon limb crosses the top and bottom of the row. x_limb_top = sqrt(m_radius**2 - min(y_top**2, m_radius**2)) x_limb_bottom = sqrt(m_radius**2 - min(y_bottom**2, m_radius**2)) # The row of tiles does not contain the x axis. The sunlit phase attains its maximum # and minimum x values at the top or bottom. if y_top * y_bottom > 0.: x_max = max(x_limb_top, x_limb_bottom) x_min = min(x_limb_top * cos(phase_angle), x_limb_bottom * cos(phase_angle)) # The row of tiles straddles the x axis: the maximal x value of the phase is the # moon's radius. else: x_max = m_radius # Terminator left of y axix: the easy case. if cos(phase_angle) < 0.: x_min = m_radius * cos(phase_angle) # Terminator to the right of y axis: The minimum x value is attained either at the # top or bottom. else: x_min = min(x_limb_top * cos(phase_angle), x_limb_bottom * cos(phase_angle)) # Construct the row of tiles. It must span the x interval [x_min, x_max]. row_of_tiles = [] # As above for the y coordinate, compute the minimum number of tiles and round up. n_cols = (x_max - x_min + 2. * self.ol_outer - ol_inner_min) / (self.im_w - ol_inner_min) n_cols_corrected = int(ceil(n_cols)) # Update the maximal number of tiles in a row. max_cols = max(max_cols, n_cols_corrected) # If there is more than one tile in the row: Increase the horizontal tile overlap in # this row so that the outer margin is as specified. if n_cols_corrected > 1: ol_inner_h = (n_cols_corrected * self.im_w - x_max + x_min - 2. * self.ol_outer) / (n_cols_corrected - 1.) else: ol_inner_h = ol_inner_min # For each tile of this row: Collect all data for this tile in a dictionary. for j in range(n_cols_corrected): tile = {} tile['row_index'] = i tile['column_index'] = j tile['column_total'] = n_cols_corrected tile['x_right'] = x_max + self.ol_outer - j * (self.im_w - ol_inner_h) tile['x_left'] = tile['x_right'] - self.im_w tile['y_top'] = y_top tile['y_bottom'] = y_bottom tile['x_center'] = (tile['x_right'] + tile['x_left']) / 2. tile['y_center'] = (tile['y_top'] + tile['y_bottom']) / 2. # rotate the (x,y) coordinates to get displacements in (RA,DE) relative to the # moon center. Note the approximate correction of the RA displacement because of # the moon's declination. [tile['delta_ra_center'], tile['delta_de_center'] ] = (Miscellaneous.rotate(pos_angle, de_center, scale_factor, flip_x, flip_y, tile['x_center'], tile['y_center'])) # Append the tile to its row. row_of_tiles.append(tile) # A row of tiles is completed, append it to the global structure. self.lists_of_tiles.append(row_of_tiles) # Put all tiles in a sequential order. The numbering goes through columns of tiles, always # from top to bottom. Depending on parameter "limb first", the process starts at the sunlit # limb or at the terminator. self.list_of_tiles_sorted = [] # Start at the sunlit limb. if self.limb_first: for j in range(max_cols, 0, -1): for i in range(n_rows_corrected): # Not in all rows there are "max_cols" tiles. if j <= len(self.lists_of_tiles[i]): self.list_of_tiles_sorted.append( self.lists_of_tiles[i][len(self.lists_of_tiles[i]) - j]) # Start at the terminator. else: for j in range(max_cols): for i in range(n_rows_corrected): if j < len(self.lists_of_tiles[i]): self.list_of_tiles_sorted.append( self.lists_of_tiles[i][len(self.lists_of_tiles[i]) - j - 1]) # Compute the (RA,DE) offsets from moon center for the midpoint on the sunlit limb. The # coordinates of this point are (m_radius, 0.) in the (x,y) coordinate system. Rotate into # (RA,DE) system. [self.delta_ra_limb_center, self.delta_de_limb_center ] = (Miscellaneous.rotate(pos_angle, de_center, scale_factor, flip_x, flip_y, m_radius, 0.))
def initialize_auto_align(self, camera_socket): """ Establish the relation between the directions of (x,y) coordinates in an idealized pixel image of the Moon (x positive to the east, y positive southwards) and the (x,y) coordinates of the normalized plane in which the tile construction is done (x positive to the right, y positive upwards). Take into account potential mirror inversion in the optical system. :param camera_socket: interface to the camera to capture videos and still images :return: fraction of alignment error as compared to width of overlap between tiles """ self.autoalign_initialized = False try: # Capture an alignment reference frame self.im_shift = ImageShift(self.configuration, camera_socket, debug=self.debug) except RuntimeError: if self.configuration.protocol_level > 0: Miscellaneous.protocol( "Auto-alignment initialization failed in capturing alignment reference frame." ) raise RuntimeError if self.configuration.protocol_level > 1: Miscellaneous.protocol("Alignment reference frame captured.") # The shift_angle is the overlap width between panorama tiles (in radians). self.shift_angle = self.im_shift.ol_angle # Three positions in the sky are defined: right shift in x direction, zero shift, and # downward shift in y direction. (x,y) are the pixel coordinates in the still images # captured with the video camera. All shifts are relative to the current coordinates of # the landmark. shift_vectors = [[self.shift_angle, 0.], [0., 0.], [0., self.shift_angle]] xy_shifts = [] for shift in shift_vectors: # Compute current coordinates of landmark, including corrections for alignment and drift (ra_landmark, de_landmark) = (self.compute_telescope_coordinates_of_landmark()) # Transform (x,y) coordinates into (ra,de) coordinates. The y-flip has to be set to -1. # because the rotate function assumes the y coordinate to point up, whereas the y pixel # coordinate is pointing down (see comment in method align. (shift_angle_ra, shift_angle_de) = Miscellaneous.rotate(self.me.pos_angle_pole, self.me.de, 1., 1., -1., shift[0], shift[1]) # Drive the telescope to the computed position in the sky. self.tel.slew_to(ra_landmark + shift_angle_ra, de_landmark + shift_angle_de) # Wait until the telescope orientation has stabilized. time.sleep( self.configuration.conf.getfloat("ASCOM", "wait interval")) try: # Capture a still image of the area around landmark and determine the shift versus # the reference frame. (x_shift, y_shift, in_cluster, outliers) = self.im_shift.shift_vs_reference() # If the image was not good enough for automatic shift determination, disable auto- # alignment. except RuntimeError as e: if self.configuration.protocol_level > 2: Miscellaneous.protocol(str(e)) raise RuntimeError if self.configuration.protocol_level > 2: Miscellaneous.protocol( "Frame captured for auto-alignment, x_shift: " + str(round(x_shift / self.im_shift.pixel_angle, 1)) + ", y_shift: " + str(round(y_shift / self.im_shift.pixel_angle, 1)) + " (pixels), # consistent shifts: " + str(in_cluster) + ", # outliers: " + str(outliers) + ".") xy_shifts.append([x_shift, y_shift]) # Subtract second position from first and third position and reverse the vector. Reason for # the reversal: The shift has been applied to the mount pointing. The shift measured in the # image is the opposite of the mount shift. shift_vector_0_measured = [ xy_shifts[1][0] - xy_shifts[0][0], xy_shifts[1][1] - xy_shifts[0][1] ] shift_vector_2_measured = [ xy_shifts[1][0] - xy_shifts[2][0], xy_shifts[1][1] - xy_shifts[2][1] ] # Compare measured shifts in x and y with the expected directions to find out if images # are mirror-inverted in x or y. self.flip_x = np.sign(shift_vector_0_measured[0]) self.flip_y = np.sign(shift_vector_2_measured[1]) if self.configuration.protocol_level > 2: if self.flip_x < 0: Miscellaneous.protocol( "Auto-alignment, image flipped horizontally.") else: Miscellaneous.protocol( "Auto-alignment, image not flipped horizontally.") if self.flip_y < 0: Miscellaneous.protocol( "Auto-alignment, image flipped vertically.") else: Miscellaneous.protocol( "Auto-alignment, image not flipped vertically.") # Determine how much the measured shifts deviate from the expected shifts in the focal # plane. If the difference is too large, auto-alignment initialization is interpreted as # not successful. error_x = abs(abs(shift_vector_0_measured[0]) - self.shift_angle) / self.shift_angle error_y = abs(abs(shift_vector_2_measured[1]) - self.shift_angle) / self.shift_angle error = max(error_x, error_y) focal_length_x = abs(shift_vector_0_measured[0] ) / self.shift_angle * self.im_shift.focal_length focal_length_y = abs(shift_vector_2_measured[1] ) / self.shift_angle * self.im_shift.focal_length if self.configuration.protocol_level > 1: Miscellaneous.protocol("Focal length measured in x direction: " + str(round(focal_length_x, 1)) + ", in y direction: " + str(round(focal_length_y, 1)) + " (mm).") if error > self.configuration.align_max_autoalign_error: if self.configuration.protocol_level > 0: Miscellaneous.protocol( "Auto-alignment initialization failed, focal length error in x: " + str(round(error_x * 100., 1)) + ", in y: " + str(round(error_y * 100., 1)) + " (percent).") raise RuntimeError else: if self.configuration.protocol_level > 0: Miscellaneous.protocol( "Auto-alignment successful, focal length error in x: " + str(round(error_x * 100., 1)) + ", in y: " + str(round(error_y * 100., 1)) + " (percent).") self.autoalign_initialized = True # Return the relative error as compared with tile overlap width. return error
def align(self, alignment_manual=True): """ Determine the current error in telescope pointing, either with the help of the user (manual mode) or automatically (auto-alignment). :param alignment_manual: True if the telescope has been aimed at landmark by the user :return: In case alignment_manual=False (auto-alignment), return the relative alignment error. The deviation of the current positioning as compared to the expected position, based on the previous alignment, is determined. The quotient of this deviation and the width of the overlap between tiles is returned. If it is too large, a complete panorama coverage cannot be guaranteed. In case of manual_alignment=True, return None. """ # Alignment is only possible after a landmark has been selected. if not self.landmark_offset_set: if self.configuration.protocol_level > 0: Miscellaneous.protocol( "Error in alignment: Landmark offset not set.") raise RuntimeError("Error: Landmark offset not set.") # Manual alignment: The telescope is aimed at the current location of the landmark. Look # up its position and proceed to alignment computation. if alignment_manual: # The telescope position is delivered by the mount driver (ra_landmark, de_landmark) = self.tel.lookup_tel_position() relative_alignment_error = None # Auto-alignment: No assumption on the current telescope pointing can be made. else: # Automatic alignment: check if auto-alignment has been initialized if not self.autoalign_initialized: raise RuntimeError( "Error: Attempt to do an auto-alignment before initialization." ) # Move telescope to expected coordinates of alignment point (ra_landmark, de_landmark) = (self.compute_telescope_coordinates_of_landmark()) self.tel.slew_to(ra_landmark, de_landmark) time.sleep( self.configuration.conf.getfloat("ASCOM", "wait interval")) try: # Measure shift against reference frame (x_shift, y_shift, in_cluster, outliers) = self.im_shift.shift_vs_reference() if self.configuration.protocol_level > 1: Miscellaneous.protocol( "New alignment frame analyzed, x_shift: " + str(round(x_shift / self.im_shift.pixel_angle, 1)) + ", y_shift: " + str(round(y_shift / self.im_shift.pixel_angle, 1)) + " (pixels), # consistent shifts: " + str(in_cluster) + ", # outliers: " + str(outliers) + ".") except RuntimeError as e: if self.configuration.protocol_level > 0: Miscellaneous.protocol("Exception in auto-alignment: " + str(e)) raise RuntimeError(str(e)) global_shift = sqrt(x_shift**2 + y_shift**2) relative_alignment_error = global_shift / self.shift_angle # Translate shifts measured in camera image into equatorial coordinates scale_factor = 1. # In tile construction (where the rotate function had been designed for) x is pointing # right and y upwards. Here, x is pointing right and y downwards. Therefore, the y flip # has to be reversed. (ra_shift, de_shift) = Miscellaneous.rotate( self.me.pos_angle_pole, self.me.de, scale_factor, self.flip_x, -1. * self.flip_y, x_shift, y_shift) if self.configuration.protocol_level > 2: Miscellaneous.protocol( "Alignment shift rotated to RA/DE: RA: " + str(round(ra_shift / self.im_shift.pixel_angle, 1)) + ", DE: " + str(round(de_shift / self.im_shift.pixel_angle, 1)) + " (pixels).") # The shift is computed as "current frame - reference". Add coordinate shifts to current # mount position to get mount setting where landmark is located as on reference frame. ra_landmark += ra_shift de_landmark += de_shift # From here on, manual and auto-alignment can be treated the same. The current mount # position is given by(ra_landmark, de_landmark). current_time = datetime.now() # Set the time of the alignment point with an accuracy better than a second. self.alignment_time = self.current_time_seconds(current_time) # Update ephemeris of moon and sun self.me.update(current_time) # Correction = telescope position minus updated ephemeris position of # landmark self.ra_correction = ra_landmark - (self.me.ra + self.ra_offset_landmark) self.de_correction = de_landmark - (self.me.de + self.de_offset_landmark) if self.configuration.protocol_level > 0: Miscellaneous.protocol( "Computing new alignment, current RA correction ('): " + str(round(degrees(self.ra_correction) * 60., 3)) + ", current DE correction ('): " + str(round(degrees(self.de_correction) * 60., 3)) + ".") if self.configuration.protocol_level > 2: Miscellaneous.protocol("More alignment info: moon center RA: " + str(round(degrees(self.me.ra), 5)) + ", moon center DE: " + str(round(degrees(self.me.de), 5)) + ", landmark RA: " + str(round(degrees(ra_landmark), 5)) + ", landmark DE: " + str(round(degrees(de_landmark), 5)) + " (all in degrees).") # Store a new alignment point alignment_point = {} alignment_point['time_string'] = str(current_time)[11:19] alignment_point['time_seconds'] = self.alignment_time alignment_point['ra_correction'] = self.ra_correction alignment_point['de_correction'] = self.de_correction self.alignment_points.append(alignment_point) self.is_aligned = True # If more than one alignment point is stored, enable drift dialog and compute drift rate # of telescope mount. if len(self.alignment_points) > 1: self.drift_dialog_enabled = True if self.default_last_drift: self.last_index = len(self.alignment_points) - 1 self.compute_drift_rate() return relative_alignment_error
def __init__(self, configuration, de_center, m_diameter, phase_angle, pos_angle): """ Read out parameters from the configuration object and compute the optimal tile coverage. :param configuration: object containing parameters set by the user :param de_center: declination of the moon's center (radians) :param m_diameter: diameter of the moon (radians) :param phase_angle: phase angle of the sunlit moon phase (0. for New Moon, Pi for Full Moon) :param pos_angle: angle (radians) between North and the "North Pole" of the sunlit phase, counted counterclockwise """ # Allocate instance variables used in Tile Visualization later. self.m_diameter = m_diameter self.phase_angle = phase_angle # Configuration data pixel_size = (configuration.conf.getfloat("Camera", "pixel size")) focal_length = (configuration.conf.getfloat("Telescope", "focal length")) im_h_pixel = configuration.conf.getint("Camera", "pixel vertical") im_w_pixel = (configuration.conf.getint("Camera", "pixel horizontal")) ol_outer_pixel = (configuration.conf.getint("Camera", "external margin pixel")) ol_inner_min_pixel = (configuration.conf.getint("Camera", "tile overlap pixel")) self.limb_first = (configuration.conf.getboolean("Workflow", "limb first")) # Height / width of the image, external margin width and tile overlap in radians self.im_h = float(im_h_pixel) * atan(pixel_size / focal_length) self.im_w = float(im_w_pixel) * atan(pixel_size / focal_length) self.ol_outer = float(ol_outer_pixel) * atan(pixel_size / focal_length) ol_inner_min = float(ol_inner_min_pixel) * atan(pixel_size / focal_length) # Auxiliary parameters used by the "rotate" method in module miscellaneous flip_x = 1. flip_y = 1. scale_factor = 1. # Compute the minimum number of tile rows needed to fulfill overlap requirements. n_rows = (m_diameter + 2. * self.ol_outer - ol_inner_min) / (self.im_h - ol_inner_min) # Round up to next integer. n_rows_corrected = int(ceil(n_rows)) # Increase the vertical tile overlap such that the external margin width is as specified. if n_rows_corrected > 1: self.ol_inner_v = (n_rows_corrected * self.im_h - m_diameter - 2. * self.ol_outer) / ( n_rows_corrected - 1.) # Only one row of tiles (very unlikely though) else: self.ol_inner_v = ol_inner_min # Initialize the tile structure: All tiles in one row form a list. These lists are collected # in "lists_of_tiles". self.lists_of_tiles = [] # Initialize the maximum number of tiles in a row. max_cols = 0 # Construct each row of tiles. The origin of the (x,y) coordinate system is at the moon # center, x pointing right, y up. x and y are in radians. m_radius = m_diameter / 2. for i in range(n_rows_corrected): # Compute the y coordinates for the top and bottom of the row. y_top = m_radius + self.ol_outer - i * (self.im_h - self.ol_inner_v) y_bottom = y_top - self.im_h # Compute the x coordinates where the moon limb crosses the top and bottom of the row. x_limb_top = sqrt(m_radius ** 2 - min(y_top ** 2, m_radius ** 2)) x_limb_bottom = sqrt(m_radius ** 2 - min(y_bottom ** 2, m_radius ** 2)) # The row of tiles does not contain the x axis. The sunlit phase attains its maximum # and minimum x values at the top or bottom. if y_top * y_bottom > 0.: x_max = max(x_limb_top, x_limb_bottom) x_min = min(x_limb_top * cos(phase_angle), x_limb_bottom * cos(phase_angle)) # The row of tiles straddles the x axis: the maximal x value of the phase is the # moon's radius. else: x_max = m_radius # Terminator left of y axix: the easy case. if cos(phase_angle) < 0.: x_min = m_radius * cos(phase_angle) # Terminator to the right of y axis: The minimum x value is attained either at the # top or bottom. else: x_min = min(x_limb_top * cos(phase_angle), x_limb_bottom * cos(phase_angle)) # Construct the row of tiles. It must span the x interval [x_min, x_max]. row_of_tiles = [] # As above for the y coordinate, compute the minimum number of tiles and round up. n_cols = (x_max - x_min + 2. * self.ol_outer - ol_inner_min) / ( self.im_w - ol_inner_min) n_cols_corrected = int(ceil(n_cols)) # Update the maximal number of tiles in a row. max_cols = max(max_cols, n_cols_corrected) # If there is more than one tile in the row: Increase the horizontal tile overlap in # this row so that the outer margin is as specified. if n_cols_corrected > 1: ol_inner_h = (n_cols_corrected * self.im_w - x_max + x_min - 2. * self.ol_outer) / ( n_cols_corrected - 1.) else: ol_inner_h = ol_inner_min # For each tile of this row: Collect all data for this tile in a dictionary. for j in range(n_cols_corrected): tile = {} tile['row_index'] = i tile['column_index'] = j tile['column_total'] = n_cols_corrected tile['x_right'] = x_max + self.ol_outer - j * (self.im_w - ol_inner_h) tile['x_left'] = tile['x_right'] - self.im_w tile['y_top'] = y_top tile['y_bottom'] = y_bottom tile['x_center'] = (tile['x_right'] + tile['x_left']) / 2. tile['y_center'] = (tile['y_top'] + tile['y_bottom']) / 2. # rotate the (x,y) coordinates to get displacements in (RA,DE) relative to the # moon center. Note the approximate correction of the RA displacement because of # the moon's declination. [tile['delta_ra_center'], tile['delta_de_center']] = ( Miscellaneous.rotate(pos_angle, de_center, scale_factor, flip_x, flip_y, tile['x_center'], tile['y_center'])) # Append the tile to its row. row_of_tiles.append(tile) # A row of tiles is completed, append it to the global structure. self.lists_of_tiles.append(row_of_tiles) # Put all tiles in a sequential order. The numbering goes through columns of tiles, always # from top to bottom. Depending on parameter "limb first", the process starts at the sunlit # limb or at the terminator. self.list_of_tiles_sorted = [] # Start at the sunlit limb. if self.limb_first: for j in range(max_cols, 0, -1): for i in range(n_rows_corrected): # Not in all rows there are "max_cols" tiles. if j <= len(self.lists_of_tiles[i]): self.list_of_tiles_sorted.append( self.lists_of_tiles[i][len(self.lists_of_tiles[i]) - j]) # Start at the terminator. else: for j in range(max_cols): for i in range(n_rows_corrected): if j < len(self.lists_of_tiles[i]): self.list_of_tiles_sorted.append( self.lists_of_tiles[i][len(self.lists_of_tiles[i]) - j - 1]) # Compute the (RA,DE) offsets from moon center for the midpoint on the sunlit limb. The # coordinates of this point are (m_radius, 0.) in the (x,y) coordinate system. Rotate into # (RA,DE) system. [self.delta_ra_limb_center, self.delta_de_limb_center] = ( Miscellaneous.rotate(pos_angle, de_center, scale_factor, flip_x, flip_y, m_radius, 0.))