Exemple #1
0
    def compute_coordinate_correction(self):
        """
        Compute the current difference between telescope and celestial coordinates, including
        alignment and (if available) drift rate.

        :return: telescope - celestial coordinates: (Offset in RA, Offset in DE)
        """

        # If an alignment has been performed, compute offsets in RA and DE.
        if self.is_aligned:
            # Set correction to static value from last alignment
            ra_offset = self.ra_correction
            de_offset = self.de_correction
            # In case drift has been determined, add time-dependent correction
            # term
            if self.is_drift_set:
                now = datetime.now()
                try:
                    fract = float(str(now)[19:24])
                except:
                    fract = 0.
                # Compute time in seconds since last alignment.
                time_diff = (time.mktime(now.timetuple()) + fract -
                             self.alignment_time)
                ra_offset += time_diff * self.drift_ra
                de_offset += time_diff * self.drift_de
        # Before the first alignment, set offsets to zero and print a warning to stdout.
        else:
            if self.configuration.protocol_level > 2:
                Miscellaneous.protocol(
                    "Info: I will apply zero coordinate correction before "
                    "alignment.")
            ra_offset = 0.
            de_offset = 0.
        return ra_offset, de_offset
    def compute_landmark_offsets(self, me, landmark):
        """
        Compute offsets in (RA, DE) relative to the moon center for the landmark feature. Take into
        account topocentric parallax and libration.

        :param me: object with positions of the sun and moon, including libration info
        :param landmark: name of the landmark (String)
        :return: offset (radians) in (RA, DE) of landmark relative to moon center
        """

        try:
            # Get selenographic longitude and latitude of landmark.
            longitude = radians(self.landmarks[landmark][0])
            latitude = radians(self.landmarks[landmark][1])
            if self.configuration.protocol_level > 0:
                print("")
                Miscellaneous.protocol("New Landmark selected: " + landmark +
                                       ", selenographic longitude: " +
                                       str(round(degrees(longitude), 3)) +
                                       ", latitude: " +
                                       str(round(degrees(latitude), 3)) + ".")
            # Perform the coordinate transformation and return the offsets (in radians)
            return self.coord_translation(me, longitude, latitude)
        except:
            # This is an internal error and should not occur.
            print("Error in landmark_selection: unknown landmark",
                  file=sys.stderr)
            return 0., 0.
Exemple #3
0
    def center_offset_to_telescope_coordinates(self, delta_ra, delta_de):
        """
        Translate offset angles relative to moon center into equatorial coordinates (RA, DE) in
        the coordinate system of the telescope mount.

        :param delta_ra: Center offset angle in ra
        :param delta_de: Center offset angle in de
        :return: Equatorial telescope mount coordinates (RA, DE)
        """

        # Compute current position of the moon.
        self.me.update(datetime.now())
        if self.configuration.protocol_level > 2:
            Miscellaneous.protocol(
                "Translating center offset to equatorial coordinates, "
                "center offsets: RA: " + str(round(degrees(delta_ra), 5)) +
                ", DE: " + str(round(degrees(delta_de), 5)) +
                ", moon position (center): RA: " +
                str(round(degrees(self.me.ra), 5)) + ", DE: " +
                str(round(degrees(self.me.de), 5)) + " (all in degrees).")
        # Add the offset to moon center coordinates.
        ra = self.me.ra + delta_ra
        de = self.me.de + delta_de
        # Translate coordinates into telescope system
        return self.ephemeris_to_telescope_coordinates(ra, de)
    def normalize_and_analyze_image(self, image_array, filename_appendix):
        """
        For an image array (as produced by the camera), optimize brightness and contrast. Store the
        image in the reference image directory. Then use ORB for keypoint detection and descriptor
        computation.

        :param image_array: Numpy array with image as produced by the camera object.
        :param filename_appendix: String to be appended to filename. The filename begins with
        the current time (hours, minutes, seconds) for later reference.
        :return: tuple with four objects: the normalized image array, the image object, the
        keypoints, and the keypoint descriptors.
        """

        # height, width = image_array.shape[:2]

        # Optimize the contrast in the image.
        normalized_image_array = self.clahe.apply(image_array)

        # Version for tests: use image (already normalized) stored at last session.
        # normalized_image_array = image_array

        # Build the normalized image from the luminance channel.
        normalized_image = fromarray(normalized_image_array, 'L')
        # Write the normalized image to disk.
        normalized_image_filename = self.build_filename() + filename_appendix
        normalized_image.save(normalized_image_filename)
        if self.configuration.protocol_level > 2:
            Miscellaneous.protocol("Still image '" + filename_appendix +
                                   " captured for auto-alignment.")
        # Use the ORB for keypoint detection
        normalized_image_kp = self.orb.detect(normalized_image_array, None)
        # Compute the descriptors with ORB
        normalized_image_kp, normalized_image_des = self.orb.compute(normalized_image_array,
                                                                     normalized_image_kp)
        return normalized_image_array, normalized_image, normalized_image_kp, normalized_image_des
Exemple #5
0
    def execute_save_stacked_image(self):

        self.set_status_bar_processing_phase("saving result")
        # Save the image as 16bit int (color or mono).
        if self.configuration.global_parameters_protocol_level > 0:
            Miscellaneous.protocol("+++ Start saving the stacked image +++",
                                   self.attached_log_file)
        self.my_timer.create_no_check('Saving the stacked image')
        self.frames.save_image(self.stacked_image_name,
                               self.stack_frames.stacked_image,
                               color=self.frames.color,
                               avoid_overwriting=False)
        self.my_timer.stop('Saving the stacked image')
        if self.configuration.global_parameters_protocol_level > 1:
            Miscellaneous.protocol(
                "           The stacked image was written to: " +
                self.stacked_image_name,
                self.attached_log_file,
                precede_with_timestamp=False)

        # If postprocessing is included after stacking, set the stacked image as input.
        if self.configuration.global_parameters_include_postprocessing:
            self.postproc_input_image = self.stack_frames.stacked_image
            self.postproc_input_name = self.stacked_image_name
            self.postprocessed_image_name = PostprocDataObject.set_file_name_processed(
                self.stacked_image_name, self.configuration.postproc_suffix,
                self.configuration.global_parameters_image_format)
            self.work_next_task_signal.emit("Postprocessing")
        else:
            self.work_next_task_signal.emit("Next job")

            # Print timing info for this job.
            self.my_timer.stop('Execution over all')
            if self.configuration.global_parameters_protocol_level > 0:
                self.my_timer.protocol(self.attached_log_file)
Exemple #6
0
    def execute_set_alignment_points(self):

        # If not executing in "automatic" mode, the APs are created on the main_gui thread.
        if self.main_gui.automatic:
            self.set_status_bar_processing_phase("creating alignment points")
            # Initialize the AlignmentPoints object.
            self.my_timer.create_no_check('Initialize alignment point object')
            self.alignment_points = AlignmentPoints(
                self.configuration,
                self.frames,
                self.rank_frames,
                self.align_frames,
                progress_signal=self.work_current_progress_signal)
            self.my_timer.stop('Initialize alignment point object')

            # Create alignment points, and create an image with wll alignment point boxes and patches.
            if self.configuration.global_parameters_protocol_level > 0:
                Miscellaneous.protocol(
                    "+++ Start creating alignment points +++",
                    self.attached_log_file)
            self.my_timer.create_no_check('Create alignment points')

            # If a ROI is selected, alignment points are created in the ROI window only.
            self.alignment_points.create_ap_grid()

            self.my_timer.stop('Create alignment points')

        self.work_next_task_signal.emit("Compute frame qualities")
Exemple #7
0
    def execute_compute_frame_qualities(self):

        if self.configuration.global_parameters_protocol_level > 1:
            Miscellaneous.protocol(
                "           Number of alignment points selected: " +
                str(len(self.alignment_points.alignment_points)) +
                ", aps dropped because too dim: " +
                str(self.alignment_points.alignment_points_dropped_dim) +
                ", aps dropped because too little structure: " +
                str(self.alignment_points.alignment_points_dropped_structure),
                self.attached_log_file,
                precede_with_timestamp=False)

        self.set_status_bar_processing_phase(
            "ranking all frames at all alignment points")
        # For each alignment point rank frames by their quality.
        self.my_timer.create_no_check('Rank frames at alignment points')
        if self.configuration.global_parameters_protocol_level > 0:
            Miscellaneous.protocol(
                "+++ Start ranking all frames at all alignment points +++",
                self.attached_log_file)
        self.alignment_points.compute_frame_qualities()
        self.my_timer.stop('Rank frames at alignment points')

        self.work_next_task_signal.emit("Stack frames")
    def set_focus_area(self):
        """
        The user may select a place on the moon where lighting conditions are optimal for setting
        the focus. This method stores the current location, so the telescope can be moved to it
        later to update the focus setting.

        The user may opt to focus on a star rather than on a moon feature (by setting the
        "focus on star" configuration parameter). In this case the focus position does not move
        with the moon among the stars. Therefore, rather than computing the center offset in this
        case the RA,DE coordinates are used directly.

        :return: -
        """

        if not self.is_aligned:
            if self.configuration.protocol_level > 0:
                Miscellaneous.protocol(
                    "Internal error: Attempt to set focus area without alignment.")
            raise RuntimeError("Cannot set focus area without alignment.")
        # Look up the current position of the telescope mount.
        (ra_focus, de_focus) = self.tel.lookup_tel_position()
        # Translate telescope position into true (RA,DE) coordinates
        (self.true_ra_focus, self.true_de_focus) = (
            self.telescope_to_ephemeris_coordinates(ra_focus, de_focus))
        if not self.configuration.conf.getboolean("Workflow", "focus on star"):
            # Compute current moon position and displacement of telescope position relative to moon
            # center.
            self.me.update(datetime.now())
            self.ra_offset_focus_area = self.true_ra_focus - self.me.ra
            self.de_offset_focus_area = self.true_de_focus - self.me.de
    def compute_landmark_offsets(self, me, landmark):
        """
        Compute offsets in (RA, DE) relative to the moon center for the landmark feature. Take into
        account topocentric parallax and libration.

        :param me: object with positions of the sun and moon, including libration info
        :param landmark: name of the landmark (String)
        :return: offset (radians) in (RA, DE) of landmark relative to moon center
        """

        try:
            # Get selenographic longitude and latitude of landmark.
            longitude = radians(self.landmarks[landmark][0])
            latitude = radians(self.landmarks[landmark][1])
            if self.configuration.protocol_level > 0:
                print("")
                Miscellaneous.protocol(
                    "New Landmark selected: " + landmark + ", selenographic longitude: " + str(
                        round(degrees(longitude), 3)) + ", latitude: " + str(
                        round(degrees(latitude), 3)) + ".")
            # Perform the coordinate transformation and return the offsets (in radians)
            return self.coord_translation(me, longitude, latitude)
        except:
            # This is an internal error and should not occur.
            print("Error in landmark_selection: unknown landmark", file=sys.stderr)
            return 0., 0.
    def compute_drift_rate(self):
        """
        Compute the drift rate of the telescope mount, based on two alignment points. By default,
        the first and last alignment point are used. Other indices may have been selected by the
        user.

        :return: -
        """

        self.is_drift_set = False
        if self.drift_disabled:
            return
        time_diff = (self.alignment_points[self.last_index]['time_seconds'] -
                     self.alignment_points[self.first_index]['time_seconds'])
        # Drift is only computed if the time difference of the alignment points is large enough.
        if time_diff < self.configuration.minimum_drift_seconds:
            return
        self.drift_ra = ((self.alignment_points[self.last_index]['ra_correction'] -
                          self.alignment_points[self.first_index]['ra_correction']) / time_diff)
        self.drift_de = ((self.alignment_points[self.last_index]['de_correction'] -
                          self.alignment_points[self.first_index]['de_correction']) / time_diff)
        if self.configuration.protocol_level > 1:
            Miscellaneous.protocol(
                "Drift rate based on alignment points " + str(self.first_index + 1) + " and " + str(
                    self.last_index + 1) + ": Drift in Ra = " + str(
                    round(degrees(self.drift_ra) * 216000., 3)) + ", drift in De = " + str(round(
                    degrees(self.drift_de) * 216000., 3)) + " (in arc min/hour).")
        # Set flag to true to indicate that a valid drift rate has been determined.
        self.is_drift_set = True
    def execute_rank_frames(self):

        self.set_status_bar_processing_phase("ranking frames")
        # Rank the frames by their overall local contrast.
        if self.configuration.global_parameters_protocol_level > 0:
            Miscellaneous.protocol("+++ Start ranking images +++",
                                   self.attached_log_file)
        self.my_timer.create_no_check('Ranking images')

        try:
            self.rank_frames = RankFrames(self.frames, self.configuration,
                                          self.work_current_progress_signal)
            self.rank_frames.frame_score()
            self.my_timer.stop('Ranking images')
        except Error as e:
            self.abort_job_signal.emit("Error: " + e.message +
                                       ", continuing with next job")
            self.my_timer.stop('Ranking images')
            return
        except Exception as e:
            self.abort_job_signal.emit("Error: " + str(e) +
                                       ", continuing with next job")
            self.my_timer.stop('Ranking images')
            return

        if self.configuration.global_parameters_protocol_level > 1:
            Miscellaneous.protocol("           Index of best frame: " +
                                   str(self.rank_frames.frame_ranks_max_index),
                                   self.attached_log_file,
                                   precede_with_timestamp=False)

        self.work_next_task_signal.emit("Align frames")
    def compute_coordinate_correction(self):
        """
        Compute the current difference between telescope and celestial coordinates, including
        alignment and (if available) drift rate.

        :return: telescope - celestial coordinates: (Offset in RA, Offset in DE)
        """

        # If an alignment has been performed, compute offsets in RA and DE.
        if self.is_aligned:
            # Set correction to static value from last alignment
            ra_offset = self.ra_correction
            de_offset = self.de_correction
            # In case drift has been determined, add time-dependent correction
            # term
            if self.is_drift_set:
                now = datetime.now()
                try:
                    fract = float(str(now)[19:24])
                except:
                    fract = 0.
                # Compute time in seconds since last alignment.
                time_diff = (time.mktime(now.timetuple()) + fract - self.alignment_time)
                ra_offset += time_diff * self.drift_ra
                de_offset += time_diff * self.drift_de
        # Before the first alignment, set offsets to zero and print a warning to stdout.
        else:
            if self.configuration.protocol_level > 2:
                Miscellaneous.protocol("Info: I will apply zero coordinate correction before "
                                       "alignment.")
            ra_offset = 0.
            de_offset = 0.
        return ra_offset, de_offset
Exemple #13
0
    def set_focus_area(self):
        """
        The user may select a place on the moon where lighting conditions are optimal for setting
        the focus. This method stores the current location, so the telescope can be moved to it
        later to update the focus setting.

        The user may opt to focus on a star rather than on a moon feature (by setting the
        "focus on star" configuration parameter). In this case the focus position does not move
        with the moon among the stars. Therefore, rather than computing the center offset in this
        case the RA,DE coordinates are used directly.

        :return: -
        """

        if not self.is_aligned:
            if self.configuration.protocol_level > 0:
                Miscellaneous.protocol(
                    "Internal error: Attempt to set focus area without alignment."
                )
            raise RuntimeError("Cannot set focus area without alignment.")
        # Look up the current position of the telescope mount.
        (ra_focus, de_focus) = self.tel.lookup_tel_position()
        # Translate telescope position into true (RA,DE) coordinates
        (self.true_ra_focus,
         self.true_de_focus) = (self.telescope_to_ephemeris_coordinates(
             ra_focus, de_focus))
        if not self.configuration.conf.getboolean("Workflow", "focus on star"):
            # Compute current moon position and displacement of telescope position relative to moon
            # center.
            self.me.update(datetime.now())
            self.ra_offset_focus_area = self.true_ra_focus - self.me.ra
            self.de_offset_focus_area = self.true_de_focus - self.me.de
    def done(self):
        """
        On exit from the frame viewer, update the stack frame size and send a completion signal.

        :return: -
        """

        # Check if a new stack size was selected.
        if (self.configuration.alignment_points_frame_number !=
                self.spinBox_number_frames.value() and
                self.configuration.alignment_points_frame_number is not None) or \
                self.configuration.alignment_points_frame_percent != \
                self.spinBox_percentage_frames.value():
            # Save the (potentially changed) stack size.
            self.configuration.alignment_points_frame_percent = \
                self.spinBox_percentage_frames.value()
            self.configuration.alignment_points_frame_number = self.spinBox_number_frames.value()

            # Write the stack size change into the protocol.
            if self.configuration.global_parameters_protocol_level > 1:
                Miscellaneous.protocol("           The user has selected a new stack size: " +
                    str(self.configuration.alignment_points_frame_number) + " frames (" +
                    str(self.configuration.alignment_points_frame_percent) + "% of all frames).",
                    self.stacked_image_log_file, precede_with_timestamp=False)

        # Send a completion message.
        if self.parent_gui is not None:
            self.signal_finished.emit(self.signal_payload)

        # Close the Window.
        plt.close()
        self.close()
Exemple #15
0
    def execute_reset_masters(self):

        # De-activate master frames.
        if self.configuration.global_parameters_protocol_level > 0:
            Miscellaneous.protocol("+++ De-activating master frames +++",
                                   self.attached_log_file,
                                   precede_with_timestamp=True)
        self.calibration.reset_masters()
    def __init__(self, main_gui):
        super(Workflow, self).__init__()
        self.main_gui = main_gui
        self.configuration = main_gui.configuration

        self.my_timer = None

        self.frames = None
        self.rank_frames = None
        self.align_frames = None
        self.alignment_points = None
        self.stack_frames = None
        self.stacked_image_name = None
        self.postprocessed_image_name = None
        self.postprocessed_image = None
        self.postproc_input_image = None
        self.postproc_input_name = None
        self.activity = None
        self.attached_log_name = None
        self.attached_log_name_new = None
        self.attached_log_file = None
        self.stdout_saved = None
        self.output_redirected = False
        self.protocol_file = None

        # Switch alignment point debugging on / off.
        self.debug_AP = False

        # The following code works on Windows and Linux systems only. It is not necessary, though.
        try:
            if platform.system() == 'Windows':
                mkl_rt = CDLL('mkl_rt.dll')
            else:
                mkl_rt = CDLL('libmkl_rt.so')

            mkl_get_max_threads = mkl_rt.mkl_get_max_threads

            def mkl_set_num_threads(cores):
                mkl_rt.mkl_set_num_threads(byref(c_int(cores)))

            mkl_set_num_threads(mkl_get_max_threads())
            if self.configuration.global_parameters_protocol_level > 1:
                Miscellaneous.protocol("Number of threads used by mkl: " +
                                       str(mkl_get_max_threads()),
                                       self.attached_log_file,
                                       precede_with_timestamp=True)
        except Exception as e:
            Miscellaneous.protocol(
                "Warning: mkl_rt.dll / libmkl_rt.so does not work (not a Windows or Linux system, "
                "or Intel Math Kernel Library not installed?). " + str(e),
                self.attached_log_file,
                precede_with_timestamp=True)

        # Create the calibration object, used for potential flat / dark corrections.
        self.calibration = Calibration(self.configuration)
    def report_error(self, message):
        """
        This method is triggered by the workflow thread via a signal when an error is to be
        reported. Depending on the protocol level, the error message is written to the
        protocol (file).

        :param message: Error message to be displayed
        :return: -
        """

        if self.configuration.global_parameters_protocol_level > 0:
            Miscellaneous.protocol(message + "\n", self.workflow.attached_log_file)
    def execute_save_postprocessed_image(self, postprocessed_image):

        # The signal payload is None only if the editor was left with "cancel" in interactive mode.
        # In this case, skip saving the result and proceed with the next job.
        if postprocessed_image is not None:
            self.set_status_bar_processing_phase("saving result")
            # Save the image as 16bit int (color or mono).
            if self.configuration.global_parameters_protocol_level > 0:
                Miscellaneous.protocol(
                    "+++ Start saving the postprocessed image +++",
                    self.attached_log_file)
            self.my_timer.create_no_check('Saving the postprocessed image')
            Frames.save_image(
                self.postprocessed_image_name,
                postprocessed_image,
                color=(len(postprocessed_image.shape) == 3),
                avoid_overwriting=False,
                header=self.configuration.global_parameters_version)
            self.my_timer.stop('Saving the postprocessed image')
            if self.configuration.global_parameters_protocol_level > 1:
                Miscellaneous.protocol(
                    "           The postprocessed image was written to: " +
                    self.postprocessed_image_name,
                    self.attached_log_file,
                    precede_with_timestamp=False)

            if self.configuration.global_parameters_protocol_level > 1:
                Miscellaneous.print_postproc_parameters(
                    self.configuration.postproc_data_object.versions[
                        self.configuration.postproc_data_object.
                        version_selected].layers, self.attached_log_file)

        self.work_next_task_signal.emit("Next job")

        # Print timing info for this job.
        self.my_timer.stop('Execution over all')
        if self.configuration.global_parameters_protocol_level > 0:
            self.my_timer.protocol(self.attached_log_file)
        if self.attached_log_file:
            self.attached_log_file.close()
            # If the attached log name was defined for a stacking job, rename it to include
            # parameter information.
            if self.attached_log_name_new and self.attached_log_name_new != self.attached_log_name:
                try:
                    remove(self.attached_log_name_new)
                except:
                    pass
                rename(self.attached_log_name, self.attached_log_name_new)
                self.attached_log_name = self.attached_log_name_new
Exemple #19
0
    def __init__(self, configuration, mark_processed, debug=False):
        """
        Initialize the camera object.

        :param configuration: object containing parameters set by the user
        :param mark_processed: a method in moon_panorama_maker which marks tiles as processed
        :param debug: if True, the socket_client (FireCapture connection) is replaced with a
        mockup object with the same interface. It does not capture videos, but returns the
        acknowledgement as the real object does.

        """

        QtCore.QThread.__init__(self)

        self.configuration = configuration

        # Register method in StartQT5 (module moon_panorama_maker) for marking tile as processed.
        self.mark_processed = mark_processed

        # The "triggered" flag is set to True in "workflow" to start an exposure.
        self.triggered = False
        # The "active" flag is looked up in "workflow" to find out if a video is being acquired.
        self.active = False
        self.terminate = False
        self.active_tile_number = -1

        # Set the parameters for the socket connection to FireCapture. FireCapture might run on a
        # different computer.
        self.host = self.configuration.conf.get("Camera", "ip address")
        self.port = self.configuration.fire_capture_port_number

        # For debugging purposes, the connection to FireCapture can be replaced with a mockup class
        # which reads still images from files. These can be used to test the autoaligh mechanism.
        if debug:
            self.mysocket = SocketClientDebug(
                self.host, self.port, self.configuration.camera_debug_delay)
            if self.configuration.protocol_level > 0:
                Miscellaneous.protocol(
                    "Camera in debug mode, still camera emulated.")
        else:
            try:
                self.mysocket = SocketClient(self.host, self.port)
            except:
                raise CameraException(
                    "Unable to establish socket connection to FireCapture, host: "
                    + self.host + ", port: " + str(self.port) + ".")
            if self.configuration.protocol_level > 0:
                Miscellaneous.protocol(
                    "Camera: Connection to FireCapture program established.")
Exemple #20
0
 def open_ascom_chooser(self):
     try:
         x = win32com.client.Dispatch("ASCOM.Utilities.Chooser")
         x.DeviceType = 'Telescope'
         driver_name = x.Choose(self.new_driver_name)
         if driver_name != "":
             self.new_driver_name = driver_name
     except:
         if self.c.protocol_level > 0:
             Miscellaneous.protocol("Unable to access the ASCOM telescope chooser. Please check"
                                    " the ASCOM platform installation.")
         Miscellaneous.show_detailed_error_message("Unable to access the ASCOM telescope "
                                                   "chooser", "Is the ASCOM Platform "
                                                              "installed on this "
                                                              "computer? Please check the "
                                                              "installation.")
Exemple #21
0
    def done(self):
        """
        On exit from the frame viewer, update the stack frame size and send a completion signal.

        :return: -
        """

        # Check if a new stack size was selected.
        if self.stack_size_changed is not None:

            # The number of frames has been set explicitly. Check if it has changed.
            if self.stack_size_changed == 'number':
                if self.spinBox_number_frames.value(
                ) != self.configuration.alignment_points_frame_number:
                    self.configuration.alignment_points_frame_number = self.spinBox_number_frames.value(
                    )
                    self.configuration.alignment_points_frame_percent = -1
                    if self.configuration.global_parameters_protocol_level > 1:
                        Miscellaneous.protocol(
                            "           The user has selected a new stack size: "
                            + str(self.configuration.
                                  alignment_points_frame_number) + " frames.",
                            self.stacked_image_log_file,
                            precede_with_timestamp=False)

            # The percentage of frames has been set. Check if it has changed.
            elif self.spinBox_percentage_frames.value(
            ) != self.configuration.alignment_points_frame_percent:
                self.configuration.alignment_points_frame_number = -1
                self.configuration.alignment_points_frame_percent = \
                    self.spinBox_percentage_frames.value()
                if self.configuration.global_parameters_protocol_level > 1:
                    Miscellaneous.protocol(
                        "           The user has selected a new stack size: " +
                        str(self.configuration.alignment_points_frame_percent)
                        + "% of all frames.",
                        self.stacked_image_log_file,
                        precede_with_timestamp=False)

        # Send a completion message.
        if self.parent_gui is not None:
            self.signal_finished.emit(self.signal_payload)

        # Close the Window.
        self.player_thread.quit()
        plt.close()
        self.close()
Exemple #22
0
    def execute_set_roi(self, y_min, y_max, x_min, x_max):

        self.set_status_bar_processing_phase("setting the ROI")
        if self.configuration.global_parameters_protocol_level > 0 and y_min != 0 or y_max != 0:
            Miscellaneous.protocol(
                "+++ Start setting a ROI and computing a new average frame +++",
                self.attached_log_file)
        self.my_timer.create_no_check('Setting ROI and new reference')
        self.align_frames.set_roi(y_min, y_max, x_min, x_max)
        self.my_timer.stop('Setting ROI and new reference')

        if self.configuration.global_parameters_protocol_level > 1 and y_min != 0 or y_max != 0:
            Miscellaneous.protocol("           ROI, set by the user: "******"<y<" + str(y_max) + ", " +
                                   str(x_min) + "<x<" + str(x_max),
                                   self.attached_log_file,
                                   precede_with_timestamp=False)

        self.work_next_task_signal.emit("Set alignment points")
    def set_landmark(self):
        """
        Let the user select the landmark used for telescope alignment and compute its offset
        from the moon center, including libration and topocentric parallax.

        :return: -
        """

        # Open a gui where the user can select among a collection of landmarks on the moon
        offsets = self.ls.select_landmark(self.me, datetime.now())
        # A landmark has been selected, store and print coordinate offsets.
        if self.ls.landmark_selected:
            (self.ra_offset_landmark, self.de_offset_landmark) = offsets
            if self.configuration.protocol_level > 1:
                Miscellaneous.protocol("Landmark offset from center RA ('): " + str(
                    round(degrees(self.ra_offset_landmark) * 60., 3)) + ", DE ('): " + str(
                    round(degrees(self.de_offset_landmark) * 60., 3)) + ".")
            self.landmark_offset_set = True
        else:
            self.landmark_offset_set = False
    def execute_create_master_dark(self, dark_names):
        # Create a new master dark.
        if self.configuration.global_parameters_protocol_level > 0:
            Miscellaneous.protocol("+++ Creating a new master dark frame +++",
                                   self.attached_log_file,
                                   precede_with_timestamp=True)
        if self.configuration.global_parameters_protocol_level > 1:
            Miscellaneous.protocol("           Input frames: " + dark_names[0],
                                   self.attached_log_file,
                                   precede_with_timestamp=False)

        try:
            self.set_main_gui_busy_signal.emit(True)
            self.calibration.create_master_dark(dark_names[0])
            self.master_dark_created_signal.emit(True)
        except Exception as e:
            if self.configuration.global_parameters_protocol_level > 0:
                self.report_error_signal.emit(
                    "Error in creating master dark frame: " + str(e))
            self.master_dark_created_signal.emit(False)
    def ephemeris_to_telescope_coordinates(self, ra, de):
        """
        Translate celestial equatorial coordinates into coordinates of telescope mount.

        :param ra: Celestial right ascension
        :param de: Celestial declination
        :return: Equatorial mount coordinates (RA, DE)
        """

        correction = self.compute_coordinate_correction()
        # Add corrections to ephemeris position to get telescope coordinates
        telescope_ra = ra + correction[0]
        telescope_de = de + correction[1]
        if self.configuration.protocol_level > 2:
            Miscellaneous.protocol("Translating equatorial to telescope coordinates, "
                                   "correction in RA: " + str(
                round(degrees(correction[0]), 5)) + ", in DE: " + str(
                round(degrees(correction[1]), 5)) + ", Telescope RA: " + str(
                round(degrees(telescope_ra), 5)) + ", Telescope DE: " + str(
                round(degrees(telescope_de), 5)) + " (all in degrees).")
        return telescope_ra, telescope_de
Exemple #26
0
    def execute_create_master_flat(self, flat_names):

        # Create a new master flat.
        if self.configuration.global_parameters_protocol_level > 0:
            Miscellaneous.protocol("+++ Creating a new master flat frame +++",
                                   self.attached_log_file,
                                   precede_with_timestamp=True)
        if self.configuration.global_parameters_protocol_level > 1:
            Miscellaneous.protocol("           Input frames: " + flat_names[0],
                                   self.attached_log_file,
                                   precede_with_timestamp=False)

        try:
            self.set_main_gui_busy_signal.emit(True)
            self.calibration.create_master_flat(flat_names[0])
            # if self.configuration.global_parameters_protocol_level > 0 and \
            #         self.calibration.master_dark_removed:
            #     Miscellaneous.protocol("           A non-matching master dark was de-activated",
            #         self.attached_log_file, precede_with_timestamp=False)
            self.master_flat_created_signal.emit(True)
        except Error as e:
            if self.configuration.global_parameters_protocol_level > 0:
                Miscellaneous.protocol(
                    "           Error in creating master flat frame: " +
                    str(e) + ", flat frame calibration de-activated",
                    self.attached_log_file,
                    precede_with_timestamp=False)
                self.master_flat_created_signal.emit(False)
Exemple #27
0
    def ephemeris_to_telescope_coordinates(self, ra, de):
        """
        Translate celestial equatorial coordinates into coordinates of telescope mount.

        :param ra: Celestial right ascension
        :param de: Celestial declination
        :return: Equatorial mount coordinates (RA, DE)
        """

        correction = self.compute_coordinate_correction()
        # Add corrections to ephemeris position to get telescope coordinates
        telescope_ra = ra + correction[0]
        telescope_de = de + correction[1]
        if self.configuration.protocol_level > 2:
            Miscellaneous.protocol(
                "Translating equatorial to telescope coordinates, "
                "correction in RA: " + str(round(degrees(correction[0]), 5)) +
                ", in DE: " + str(round(degrees(correction[1]), 5)) +
                ", Telescope RA: " + str(round(degrees(telescope_ra), 5)) +
                ", Telescope DE: " + str(round(degrees(telescope_de), 5)) +
                " (all in degrees).")
        return telescope_ra, telescope_de
 def protocol(self, logfile):
     Miscellaneous.protocol("", logfile, precede_with_timestamp=False)
     Miscellaneous.protocol(
         "           --------------------------------------------------\n"
         "           Status of time counters:",
         logfile,
         precede_with_timestamp=False)
     for name in self.counters.keys():
         Miscellaneous.protocol("           {0:40} {1:8.3f}".format(
             name, self.counters[name][0]),
                                logfile,
                                precede_with_timestamp=False)
     Miscellaneous.protocol(
         "           --------------------------------------------------\n",
         logfile,
         precede_with_timestamp=False)
Exemple #29
0
    def execute_postprocess_image(self):

        if self.configuration.global_parameters_protocol_level > 0:
            Miscellaneous.protocol("+++ Start postprocessing +++",
                                   self.attached_log_file)
        self.my_timer.create_no_check('Conputing image postprocessing')

        # Initialize the new image with the original image.
        self.postprocessed_image = self.postproc_input_image

        # Apply all sharpening layers of the postprocessing version selected last time.
        version_index = self.configuration.postproc_data_object.version_selected
        postproc_layers = self.configuration.postproc_data_object.versions[
            version_index].layers
        for layer in postproc_layers:
            self.postprocessed_image = Miscellaneous.gaussian_sharpen(
                self.postprocessed_image,
                layer.amount,
                layer.radius,
                luminance_only=layer.luminance_only)
        self.my_timer.stop('Conputing image postprocessing')

        self.work_next_task_signal.emit("Save postprocessed image")
Exemple #30
0
    def set_landmark(self):
        """
        Let the user select the landmark used for telescope alignment and compute its offset
        from the moon center, including libration and topocentric parallax.

        :return: -
        """

        # Open a gui where the user can select among a collection of landmarks on the moon
        offsets = self.ls.select_landmark(self.me, datetime.now())
        # A landmark has been selected, store and print coordinate offsets.
        if self.ls.landmark_selected:
            (self.ra_offset_landmark, self.de_offset_landmark) = offsets
            if self.configuration.protocol_level > 1:
                Miscellaneous.protocol(
                    "Landmark offset from center RA ('): " +
                    str(round(degrees(self.ra_offset_landmark) * 60., 3)) +
                    ", DE ('): " +
                    str(round(degrees(self.de_offset_landmark) * 60., 3)) +
                    ".")
            self.landmark_offset_set = True
        else:
            self.landmark_offset_set = False
Exemple #31
0
    def compute_drift_rate(self):
        """
        Compute the drift rate of the telescope mount, based on two alignment points. By default,
        the first and last alignment point are used. Other indices may have been selected by the
        user.

        :return: -
        """

        self.is_drift_set = False
        if self.drift_disabled:
            return
        time_diff = (self.alignment_points[self.last_index]['time_seconds'] -
                     self.alignment_points[self.first_index]['time_seconds'])
        # Drift is only computed if the time difference of the alignment points is large enough.
        if time_diff < self.configuration.minimum_drift_seconds:
            return
        self.drift_ra = (
            (self.alignment_points[self.last_index]['ra_correction'] -
             self.alignment_points[self.first_index]['ra_correction']) /
            time_diff)
        self.drift_de = (
            (self.alignment_points[self.last_index]['de_correction'] -
             self.alignment_points[self.first_index]['de_correction']) /
            time_diff)
        if self.configuration.protocol_level > 1:
            Miscellaneous.protocol(
                "Drift rate based on alignment points " +
                str(self.first_index + 1) + " and " +
                str(self.last_index + 1) + ": Drift in Ra = " +
                str(round(degrees(self.drift_ra) * 216000., 3)) +
                ", drift in De = " +
                str(round(degrees(self.drift_de) * 216000., 3)) +
                " (in arc min/hour).")
        # Set flag to true to indicate that a valid drift rate has been determined.
        self.is_drift_set = True
    def center_offset_to_telescope_coordinates(self, delta_ra, delta_de):
        """
        Translate offset angles relative to moon center into equatorial coordinates (RA, DE) in
        the coordinate system of the telescope mount.

        :param delta_ra: Center offset angle in ra
        :param delta_de: Center offset angle in de
        :return: Equatorial telescope mount coordinates (RA, DE)
        """

        # Compute current position of the moon.
        self.me.update(datetime.now())
        if self.configuration.protocol_level > 2:
            Miscellaneous.protocol("Translating center offset to equatorial coordinates, "
                                   "center offsets: RA: " + str(
                round(degrees(delta_ra), 5)) + ", DE: " + str(
                round(degrees(delta_de), 5)) + ", moon position (center): RA: " + str(
                round(degrees(self.me.ra), 5)) + ", DE: " + str(
                round(degrees(self.me.de), 5)) + " (all in degrees).")
        # Add the offset to moon center coordinates.
        ra = self.me.ra + delta_ra
        de = self.me.de + delta_de
        # Translate coordinates into telescope system
        return self.ephemeris_to_telescope_coordinates(ra, de)
Exemple #33
0
    def done(self):
        """
        On exit from the frame viewer, update the selection status of all frames and send a
        completion signal.

        :return: -
        """

        # Check if the status of frames has changed.
        indices_included = []
        indices_excluded = []
        for index in range(self.frames.number_original):
            if self.index_included[
                    index] and not self.frames.index_included[index]:
                indices_included.append(index)
                self.frames.index_included[index] = True
            elif not self.index_included[index] and self.frames.index_included[
                    index]:
                indices_excluded.append(index)
                self.frames.index_included[index] = False

        # Write the changes in frame selection to the protocol.
        if self.configuration.global_parameters_protocol_level > 1:
            if indices_included:
                Miscellaneous.protocol(
                    "           The user has included the following frames into the stacking "
                    "workflow: " + str([item + 1
                                        for item in indices_included]),
                    self.stacked_image_log_file,
                    precede_with_timestamp=False)
            if indices_excluded:
                Miscellaneous.protocol(
                    "           The user has excluded the following frames from the stacking "
                    "workflow: " + str([item + 1
                                        for item in indices_excluded]),
                    self.stacked_image_log_file,
                    precede_with_timestamp=False)
            frames_remaining = sum(self.frames.index_included)
            if frames_remaining != self.frames.number:
                Miscellaneous.protocol(
                    "           " + str(frames_remaining) +
                    " frames will be used in the stacking workflow.",
                    self.stacked_image_log_file,
                    precede_with_timestamp=False)

        # Send a completion message. The "execute_rank_frames" method is triggered on the workflow
        # thread. The signal payload is True if the status was changed for at least one frame.
        # In this case, the index translation table is updated before the frame ranking starts.
        if self.parent_gui is not None:
            self.signal_finished.emit()

        # Close the Window.
        self.player_thread.quit()
        self.close()
Exemple #34
0
    def execute_stack_frames(self):

        self.set_status_bar_processing_phase("stacking frames")
        # Allocate StackFrames object.
        self.stack_frames = StackFrames(
            self.configuration,
            self.frames,
            self.align_frames,
            self.alignment_points,
            self.my_timer,
            progress_signal=self.work_current_progress_signal,
            debug=self.debug_AP,
            create_image_window_signal=self.create_image_window_signal,
            update_image_window_signal=self.update_image_window_signal,
            terminate_image_window_signal=self.terminate_image_window_signal)

        # Stack all frames.
        if self.configuration.global_parameters_protocol_level > 0:
            Miscellaneous.protocol(
                "+++ Start stacking " + str(self.alignment_points.stack_size) +
                " frames +++", self.attached_log_file)
        self.stack_frames.stack_frames()

        if self.configuration.global_parameters_protocol_level > 1 and \
                len(self.alignment_points.alignment_points) > 0:
            Miscellaneous.protocol(
                "\n           Distribution of shifts at alignment points:",
                self.attached_log_file,
                precede_with_timestamp=False)
            Miscellaneous.protocol(self.stack_frames.print_shift_table() +
                                   "\n",
                                   self.attached_log_file,
                                   precede_with_timestamp=False)

        self.set_status_bar_processing_phase("merging AP patches")
        # Merge the stacked alignment point buffers into a single image.
        if self.configuration.global_parameters_protocol_level > 0:
            Miscellaneous.protocol(
                "+++ Start merging all alignment patches and the background +++",
                self.attached_log_file)
        self.stack_frames.merge_alignment_point_buffers()

        self.work_next_task_signal.emit("Save stacked image")
    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
Exemple #36
0
    def run(self):
        while not self.terminate:
            if self.triggered:
                self.triggered = False
                self.active = True
                # Acquire "repetition_count" videos by triggering FireCapture through the socket.
                # Setting a repetition count > 1 allows the consecutive acquisition of more than
                # one video (e.g. for exposures with different filters.
                repetition_count = self.configuration.conf.getint(
                    "Camera", "repetition count")
                for video_number in range(repetition_count):
                    if video_number > 0:
                        # If more than one video per tile is to be recorded, insert a short wait
                        # time. Otherwise FireCapture might get stuck.
                        time.sleep(self.configuration.
                                   camera_time_between_multiple_exposures)
                    if self.configuration.protocol_level > 0:
                        Miscellaneous.protocol(
                            "Camera: Send trigger to FireCapture, tile: " +
                            str(self.active_tile_number) +
                            ", repetition number: " + str(video_number) + ".")
                    try:
                        # The tile number is encoded in the message. The FireCapture plugin appends
                        # this message to the video file names (to keep the files apart later).
                        msg = "_Tile-{0:0>3}".format(self.active_tile_number)
                        self.mysocket.mysend(msg)
                    except Exception as e:
                        if self.configuration.protocol_level > 0:
                            Miscellaneous.protocol(
                                "Camera, Error message in trigger: " + str(e))
                    if self.configuration.protocol_level > 2:
                        Miscellaneous.protocol(
                            "Camera: Wait for FireCapture to finish exposure" +
                            ".")
                    try:
                        # Wait for FireCapture to finish the exposure.
                        ack_length = 1
                        ack = self.mysocket.myreceive(ack_length)
                    except Exception as e:
                        if self.configuration.protocol_level > 0:
                            Miscellaneous.protocol(
                                "Camera, Error message in ack: " + str(e))
                    if self.configuration.protocol_level > 2:
                        Miscellaneous.protocol(
                            "Camera: acknowledgement from FireCapture = " +
                            str(ack) + ".")

                # All videos for this tile are acquired, mark tile as processed.
                self.mark_processed()
                # Trigger method "signal_from_camera" in moon_panorama_maker
                self.camera_signal.emit()
                if self.configuration.protocol_level > 0:
                    Miscellaneous.protocol(
                        "Camera, all videos for tile " +
                        str(self.active_tile_number) +
                        " captured, signal (tile processed) emitted.")
                self.active = False
            # Insert a wait time to keep the CPU usage low.
            time.sleep(self.configuration.polling_interval)

        self.mysocket.close()
        if self.configuration.protocol_level > 0:
            Miscellaneous.protocol(
                "Camera: Connection to FireCapture program closed.")
    def __init__(self, configuration, camera_socket, debug=False):
        """
        Initialize the ImageShift object, capture the reference frame and find keypoints in the
        reference frame.

        :param configuration: object containing parameters set by the user
        :param camera_socket: the socket_client object used by the camera
        :param debug: if set to True, display keypoints and matches in Matplotlib windows.
        """

        self.configuration = configuration
        self.camera_socket = camera_socket

        # Initialize instance variables.
        self.shifted_image_array = None
        self.shifted_image = None
        self.shifted_image_kp = None
        self.shifted_image_des = None

        # Get camera and telescope parameters.
        pixel_size = (self.configuration.conf.getfloat("Camera", "pixel size"))
        self.focal_length = (self.configuration.conf.getfloat("Telescope", "focal length"))
        ol_inner_min_pixel = (self.configuration.conf.getint("Camera", "tile overlap pixel"))
        # The still pictures produced by the camera are reduced both in x and y pixel directions
        # by "compression_factor". Set the compression factor such that the overlap between tiles
        # is resolved in a given number of pixels (pixels_in_overlap_width). This resolution should
        # be selected such that the telescope pointing can be determined precisely enough for
        # auto-alignment.
        self.compression_factor = ol_inner_min_pixel / self.configuration.pixels_in_overlap_width
        # Compute the angle corresponding to a single pixel in the focal plane.
        self.pixel_angle = atan(pixel_size / self.focal_length)
        # Compute the angle corresponding to the overlap between tiles.
        self.ol_angle = ol_inner_min_pixel * self.pixel_angle
        # The scale value is the angle corresponding to a single pixel in the compressed camera
        # images.
        self.scale = self.compression_factor * self.pixel_angle
        self.debug = debug

        # During auto-alignment all still images captured are stored in a directory in the user's
        # home directory. If such a directory is found from an old MPM run, delete it first.
        self.image_dir = os.path.join(self.configuration.home,
                                      ".MoonPanoramaMaker_alignment_images")

        # If the directory is old, delete it first.
        if os.path.exists(self.image_dir) and time.time() - os.path.getmtime(
                self.image_dir) > self.configuration.alignment_pictures_retention_time:
            try:
                shutil.rmtree(self.image_dir)
            except:
                raise RuntimeError

        # If the directory does not exist or has just been deleted, create a new one.
        if not os.path.exists(self.image_dir):
                # Create directory for still images. In Windows this operation sometimes fails.
                # Therefore, retry until the operation is successful.
                success = False
                for retry in range(self.configuration.polling_time_out_count):
                    try:
                        os.mkdir(self.image_dir)
                        success = True
                        break
                    except:
                        if self.configuration.protocol_level > 1:
                            Miscellaneous.protocol(
                                "Warning: In imageShift, mkdir failed, retrying...")
                        plt.pause(0.1)
                # Raise a runtime error if all loop iterations were unsuccessful.
                if not success:
                    raise RuntimeError

        # The counter is used to number the alignment images captured during auto-alignment.
        self.alignment_image_counter = 0

        # Create CLAHE and ORB objects.
        self.clahe = cv2.createCLAHE(clipLimit=self.configuration.clahe_clip_limit, tileGridSize=(
            self.configuration.clahe_tile_grid_size, self.configuration.clahe_tile_grid_size))
        self.orb = cv2.ORB_create(WTA_K=self.configuration.orb_wta_k,
                                  nfeatures=self.configuration.orb_nfeatures,
                                  scoreType=cv2.ORB_HARRIS_SCORE,
                                  edgeThreshold=self.configuration.orb_edge_threshold,
                                  patchSize=self.configuration.orb_patch_size,
                                  scaleFactor=self.configuration.orb_scale_factor,
                                  nlevels=self.configuration.orb_n_levels)
        # Create BFMatcher object
        self.bf = cv2.BFMatcher(cv2.NORM_HAMMING2, crossCheck=True)

        try:
            if self.configuration.camera_debug:
                # For debugging purposes: use stored image (already compressed) from observation run
                # Begin with first stored image for every autoalignment initialization.
                self.camera_socket.image_counter = 0
                (reference_image_array, width, height,
                 dynamic) = self.camera_socket.acquire_still_image(1)
            else:
                # Capture the reference image which shows perfect alignment, apply compression.
                (reference_image_array, width, height,
                 dynamic) = self.camera_socket.acquire_still_image(self.compression_factor)

            # Normalize brightness and contrast, and determine keypoints and their descriptors.
            (self.reference_image_array, self.reference_image, self.reference_image_kp,
             self.reference_image_des) = self.normalize_and_analyze_image(reference_image_array,
                                                                    "alignment_reference_image.pgm")
        except:
            raise RuntimeError

        # Draw only keypoints location, not size and orientation
        if self.debug:
            img = cv2.drawKeypoints(self.reference_image_array, self.reference_image_kp,
                                    self.reference_image_array)
            plt.imshow(img)
            plt.show()
Exemple #38
0
    def __init__(self, parent_gui, configuration, frames, rank_frames,
                 stacked_image_log_file, signal_finished):
        """
        Initialization of the widget.

        :param parent_gui: Parent GUI object
        :param configuration: Configuration object with parameters
        :param frames: Frames object with all video frames
        :param rank_frames: RankFrames object with global quality ranks (between 0. and 1.,
                            1. being optimal) for all frames
        :param stacked_image_log_file: Log file to be stored with results, or None.
        :param signal_finished: Qt signal to trigger the next activity when the viewer exits.
        """

        super(FrameSelectorWidget, self).__init__(parent_gui)
        self.setupUi(self)

        # Keep references to upper level objects.
        self.parent_gui = parent_gui
        self.configuration = configuration
        self.stacked_image_log_file = stacked_image_log_file
        self.signal_finished = signal_finished
        self.frames = frames
        self.index_included = frames.index_included.copy()
        self.quality_sorted_indices = rank_frames.quality_sorted_indices
        self.rank_indices = rank_frames.rank_indices

        # Start with ordering frames by quality. This can be changed by the user using a radio
        # button.
        self.frame_ordering = "quality"

        # Initialize the frame list selection.
        self.items_selected = None
        self.indices_selected = None

        # Set colors for the frame list.
        self.background_included = QtGui.QColor(130, 255, 130)
        self.foreground_included = QtGui.QColor(0, 0, 0)
        self.background_excluded = QtGui.QColor(120, 120, 120)
        self.foreground_excluded = QtGui.QColor(255, 255, 255)

        self.addButton.clicked.connect(self.use_triggered)
        self.removeButton.clicked.connect(self.not_use_triggered)

        # Be careful: Indices are counted from 0, while widget contents are counted from 1 (to make
        # it easier for the user.
        self.quality_index = 0
        self.frame_index = self.quality_sorted_indices[self.quality_index]

        # Set up the frame selector and put it in the upper left corner.
        self.frame_selector = VideoFrameSelector(self.frames,
                                                 self.index_included,
                                                 self.frame_index)
        self.frame_selector.setObjectName("frame_selector")
        self.gridLayout.addWidget(self.frame_selector, 0, 0, 2, 3)

        # Initialize the list widget.
        self.fill_list_widget()
        self.listWidget.setSelectionMode(
            QtWidgets.QAbstractItemView.ExtendedSelection)
        self.listWidget.installEventFilter(self)
        self.listWidget.itemClicked.connect(self.select_items)
        self.listWidget.currentRowChanged.connect(self.synchronize_slider)

        # Group widget elements which are to be blocked during player execution in a list.
        self.widget_elements = [
            self.listWidget, self.slider_frames, self.addButton,
            self.removeButton, self.pushButton_play,
            self.GroupBox_frame_sorting
        ]

        # Initialize a variable for communication with the frame_player object later.
        self.run_player = False

        # Create the frame player thread and start it. The player displays frames in succession.
        # It is pushed on a different thread because otherwise the user could not stop it before it
        # finishes.
        self.player_thread = QtCore.QThread()
        self.frame_player = FramePlayer(self)
        self.frame_player.moveToThread(self.player_thread)
        self.frame_player.block_widgets_signal.connect(self.block_widgets)
        self.frame_player.unblock_widgets_signal.connect(self.unblock_widgets)
        self.frame_player.set_photo_signal.connect(
            self.frame_selector.setPhoto)
        self.frame_player.set_slider_value.connect(self.slider_frames.setValue)
        self.frame_player_start_signal.connect(self.frame_player.play)
        self.player_thread.start()

        # Initialization of GUI elements
        self.slider_frames.setMinimum(1)
        self.slider_frames.setMaximum(self.frames.number)
        self.slider_frames.setValue(self.quality_index + 1)
        self.radioButton_quality.setChecked(True)

        self.gridLayout.setColumnStretch(0, 7)
        self.gridLayout.setColumnStretch(1, 0)
        self.gridLayout.setColumnStretch(2, 0)
        self.gridLayout.setColumnStretch(3, 0)
        self.gridLayout.setColumnStretch(4, 1)
        self.gridLayout.setRowStretch(0, 0)
        self.gridLayout.setRowStretch(1, 0)

        # Connect signals with slots.
        self.buttonBox.accepted.connect(self.done)
        self.buttonBox.rejected.connect(self.reject)
        self.slider_frames.valueChanged.connect(self.slider_frames_changed)
        self.pushButton_play.clicked.connect(self.pushbutton_play_clicked)
        self.pushButton_stop.clicked.connect(self.pushbutton_stop_clicked)
        self.radioButton_quality.toggled.connect(
            self.radiobutton_quality_changed)

        if self.configuration.global_parameters_protocol_level > 0:
            Miscellaneous.protocol("+++ Start selecting frames +++",
                                   self.stacked_image_log_file)
Exemple #39
0
 def report_calibration_error(self, message):
     if self.configuration.global_parameters_protocol_level > 0:
         Miscellaneous.protocol("           " + message,
                                self.workflow.attached_log_file,
                                precede_with_timestamp=False)
Exemple #40
0
    def run(self):
        """
        Execute the workflow thread. Its main part is a permanent loop which looks for activity
        flags set by the main gui. When a flag is true, the corresponding action is performed.
        On completion, a signal is emitted.

        :return: -
        """

        # Main workflow loop.
        while not self.exiting:

            # Re-direct stdout to a file if requested in configuration.
            if self.output_channel_initialization_flag:
                self.output_channel_initialization_flag = False
                # Action required if configuration value does not match current redirection status.
                if self.gui.configuration.conf.getboolean('Workflow', 'protocol to file') != \
                        self.output_redirected:
                    # Output currently redirected. Reset to stdout.
                    if self.output_redirected:
                        sys.stdout = self.stdout_saved
                        self.output_redirected = False
                    # Currently set to stdout, redirect to file now.
                    else:
                        try:
                            self.protocol_file = open(self.gui.configuration.protocol_filename, 'a')
                            sys.stdout = self.protocol_file
                            self.output_redirected = True
                        except IOError:
                            pass
                # print ("Signal the main GUI that the output channel is initialized.")
                # Signal the main GUI that the output channel is initialized.
                self.output_channel_initialized_signal.emit()

            # Initialize the telescope object.
            elif self.telescope_initialization_flag:
                self.telescope_initialization_flag = False
                # If a telescope driver is active, first terminate it:
                if self.telescope_connected:
                    self.telescope.terminate()
                    time.sleep(4. * self.gui.configuration.polling_interval)
                    self.telescope_connected = False
                # Connect the telescope driver specified in configuration.
                try:
                    self.telescope = Telescope(self.gui.configuration)
                    # Register new telescope object with the alignment object.
                    self.al.set_telescope(self.telescope)
                    self.telescope_connected = True
                    # Signal the main GUI that the telescope driver is initialized.
                    self.telescope_initialized_signal.emit()
                except TelescopeException as e:
                    # The telescope driver does not work properly. Signal the main GUI.
                    if self.gui.configuration.protocol_level > 0:
                        Miscellaneous.protocol("Telescope initialization failed: " + str(e))
                    self.telescope_failed_signal.emit(str(e))

            # Initialize the camera object.
            elif self.camera_initialization_flag:
                self.camera_initialization_flag = False
                # If the camera is connected, disconnect it now.
                if self.camera_connected:
                    self.camera.terminate = True
                    time.sleep(4. * self.gui.configuration.polling_interval)
                    self.camera_connected = False
                # If camera automation is on, create a Camera object and connect the camera.
                if self.gui.configuration.conf.getboolean("Workflow", "camera automation"):
                    try:
                        self.camera = Camera(self.gui.configuration, self.gui.mark_processed,
                                             debug=self.gui.configuration.camera_debug)
                        self.camera.start()
                        self.camera_connected = True
                        # Signal the main GUI that the camera is initialized.
                        self.camera_initialized_signal.emit()
                    except CameraException as e:
                        if self.gui.configuration.protocol_level > 0:
                            Miscellaneous.protocol("Camera initialization failed. " + str(e))
                        self.camera_failed_signal.emit(str(e))
                else:
                    # No camera automation: Signal the main GUI that the camera is initialized,
                    # anyway. Otherwise the execution environment build would not be finished.
                    self.camera_initialized_signal.emit()

            # Initialize a new tesselation of the current moon phase and create the tile
            # visualization window.
            elif self.new_tesselation_flag:
                self.new_tesselation_flag = False

                # Initialize some instance variables.
                self.active_tile_number = -1
                self.repeat_from_here = -1
                self.all_tiles_recorded = False

                # Set the time to current time and create a new moon ephemeris object.
                self.date_time = datetime.now()
                self.me = MoonEphem(self.gui.configuration, self.date_time,
                                    debug=self.gui.configuration.ephemeris_debug)
                # Register the new ephemeris object with the alignment object.
                self.al.set_moon_ephem(self.me)

                de_center = self.me.de
                m_diameter = self.me.diameter
                phase_angle = self.me.phase_angle
                pos_angle = self.me.pos_angle_pole
                # Compute the tesselation of the sunlit moon phase.
                self.tc = TileConstructor(self.gui.configuration, de_center, m_diameter,
                                          phase_angle, pos_angle)

                # Write the initialization message to stdout / file:
                if self.gui.configuration.protocol_level > 0:
                    print("")
                    Miscellaneous.protocol("MoonPanoramaMaker (re)started.\n           "
                        "----------------------------------------------------\n" + "           " +
                        str(datetime.now())[:10] + " " + self.gui.configuration.version + "\n" +
                        "           ----------------------------------------------------")
                    Miscellaneous.protocol(
                        "Moon center RA: " + str(round(degrees(self.me.ra), 5)) + ", DE: " + str(
                            round(degrees(self.me.de), 5)) + " (degrees), " + "diameter: " + str(
                            round(degrees(m_diameter) * 60.,
                                  3)) + " ('),\n" + "           phase_angle: " + str(
                            round(degrees(phase_angle), 2)) + ", pos_angle: " + str(
                            round(degrees(pos_angle), 2)) + " (degrees).")

                self.tesselation_created = True
                # print ("Signal the main GUI that the tesselation is initialized.")
                # Signal the main GUI that the tesselation is initialized.
                self.tesselation_initialized_signal.emit()

            # Slew the telescope to the coordinates of the alignment point.
            elif self.slew_to_alignment_point_flag:
                self.slew_to_alignment_point_flag = False
                # First calibrate the north/south/east/west directions of the mount
                # This would not be necessary every time!
                self.telescope.calibrate()
                # Compute alignment point coordinates and instruct telescope to move there.
                (ra_landmark, de_landmark) = (self.al.compute_telescope_coordinates_of_landmark())
                if self.gui.configuration.protocol_level > 0:
                    Miscellaneous.protocol("Moving telescope to alignment point.")
                self.telescope.slew_to(ra_landmark, de_landmark)
                # Depending on the context where this activity was triggered emit different signals.
                if self.gui.autoalign_enabled:
                    # In auto-alignment mode: Trigger method "autoalignment_point_reached" in gui.
                    self.autoalignment_point_reached_signal.emit()
                else:
                    # In manual alignment mode: Trigger method "alignment_point_reached" in gui.
                    self.alignment_point_reached_signal.emit()

            # The mount has been aimed manually at the exact landmark location. Define an alignment
            # point.
            elif self.perform_alignment_flag:
                self.perform_alignment_flag = False
                if self.gui.configuration.protocol_level > 0:
                    print("")
                    Miscellaneous.protocol("Performing manual alignment.")
                self.al.align(alignment_manual=True)
                # Trigger method "alignment_performed" in gui.
                self.alignment_performed_signal.emit()

            # The mount has been aimed manually at the exact landmark location. Initialize
            # auto-alignment by taking a reference frame with the camera. This operation may fail
            # (e.g. if reference frame shows too little detail). Inform gui about success via a
            # signal argument.
            elif self.perform_autoalignment_flag:
                self.perform_autoalignment_flag = False
                if self.gui.configuration.protocol_level > 0:
                    print("")
                    Miscellaneous.protocol("Trying to initialize auto-alignment.")
                # Try to initialize auto-alignment. Signal (caught in moon_panorama_maker in
                # method "autoalignment_performed" carries info on success / failure as boolean.
                try:
                    self.al.initialize_auto_align(self.camera.mysocket)
                    # Initialize list of tiles captured from now on. If at the next auto-alignment
                    # the pointing precision is too low, they have to be repeated.
                    self.tile_indices_since_last_autoalign = []
                    # Signal success to gui, start method "autoalign_performed" there.
                    self.autoalignment_performed_signal.emit(True)
                    if self.gui.configuration.protocol_level > 0:
                        Miscellaneous.protocol("Auto-alignment initialization successful.")
                except RuntimeError:
                    # Signal failure to gui, start method "autoalign_performed" there.
                    self.autoalignment_performed_signal.emit(False)
                    if self.gui.configuration.protocol_level > 0:
                        Miscellaneous.protocol("Auto-alignment initialization failed.")

            # Slew the telescope to the moon's limb midpoint. Triggered by "perform_camera_rotation"
            # in gui.
            elif self.slew_to_moon_limb_flag:
                self.slew_to_moon_limb_flag = False
                # Compute coordinates of limb center point and slew telescope there.
                (ra, de) = self.al.center_offset_to_telescope_coordinates(
                    self.tc.delta_ra_limb_center, self.tc.delta_de_limb_center)
                if self.gui.configuration.protocol_level > 0:
                    print("")
                    Miscellaneous.protocol("Moving telescope to Moon limb.")
                self.telescope.slew_to(ra, de)
                if self.gui.configuration.protocol_level > 1:
                    Miscellaneous.protocol("Moon speed (arc min./hour), RA: " + str(
                        round(degrees(self.me.rate_ra) * 216000., 1)) + ", DE: " + str(
                        round(degrees(self.me.rate_de) * 216000., 1)) + ".")
                # Signal success to gui, start method "prompt_camera_rotated_acknowledged" in gui.
                self.moon_limb_centered_signal.emit()

            # Memorize the current telescope location as focus area. Triggered by method
            # "finish_set_focus_area" in gui.
            elif self.set_focus_area_flag:
                self.set_focus_area_flag = False
                self.al.set_focus_area()
                if self.gui.configuration.protocol_level > 1:
                    if self.gui.configuration.conf.getboolean("Workflow", "focus on star"):
                        Miscellaneous.protocol("Location of focus star saved, RA: " + str(
                            round(degrees(self.al.true_ra_focus), 5)) + ", DE: " + str(
                            round(degrees(self.al.true_de_focus), 5)) + " (all in degrees).")
                    else:
                        Miscellaneous.protocol(
                            "Location of focus area saved, offset from center RA ('): " + str(
                                round(degrees(self.al.ra_offset_focus_area) * 60.,
                                      3)) + ", DE ('): " + str(
                                round(degrees(self.al.de_offset_focus_area) * 60., 3)) + ".")
                # Start method "set_focus_area_finished" in gui.
                self.focus_area_set_signal.emit()

            # Move telescope to the focus area. Triggered by method "goto_focus_area" in gui.
            elif self.goto_focus_area_flag:
                self.goto_focus_area_flag = False
                (ra_focus, de_focus) = (self.al.compute_telescope_coordinates_of_focus_area())
                if self.gui.configuration.protocol_level > 0:
                    print("")
                    if self.gui.configuration.conf.getboolean("Workflow", "focus on star"):
                        Miscellaneous.protocol("Moving telescope to focus star.")
                    else:
                        Miscellaneous.protocol("Moving telescope to focus area.")
                self.telescope.slew_to(ra_focus, de_focus)

            # This is the most complicated activity of this thread. It is triggered in three
            # different situations (see method "start_continue_recording" in gui).
            elif self.slew_to_tile_and_record_flag:
                self.slew_to_tile_and_record_flag = False
                # Maximum time between auto-alignments has passed, do a new alignment
                if self.al.autoalign_initialized and self.al.seconds_since_last_alignment() > \
                        self.gui.max_seconds_between_autoaligns:
                    if self.gui.configuration.protocol_level > 0:
                        print("")
                        Miscellaneous.protocol("Trying to perform auto-alignment.")
                    self.set_text_browser_signal.emit("Trying to perform auto-alignment.")
                    # For test purposes only! Repeat alignments several times. In production mode
                    # set repetition count to 1 (in configuration).
                    auto_alignment_disabled = False
                    for repetition_index in range(self.gui.configuration.align_repetition_count):
                        try:
                            # Perform an auto-alignment. Return value gives size of correction
                            # relative to width of overlap between tiles (between 0. and 1.).
                            relative_alignment_error = self.al.align(alignment_manual=False)
                            # If enough alignment points are set, enable drift correction dialog
                            # button.
                            if self.al.drift_dialog_enabled:
                                self.gui.change_saved_key_status(
                                    self.gui.ui.configure_drift_correction, True)
                            # On first iteration only: check if time between alignments is to be
                            # adjusted.
                            if repetition_index == 0:
                                # If error too large, reduce time between auto-alignments (within
                                #  bounds
                                # given by parameters "min_autoalign_interval" and
                                # "max_autoalign_interval".
                                if relative_alignment_error > self.gui.max_alignment_error:
                                    self.gui.max_seconds_between_autoaligns = max((
                                            self.gui.max_seconds_between_autoaligns /
                                            self.gui.configuration.align_interval_change_factor),
                                        self.gui.min_autoalign_interval)
                                    if self.gui.configuration.protocol_level > 0:
                                        Miscellaneous.protocol(
                                            "Auto-alignment inaccurate: Error is " + str(round(
                                            relative_alignment_error / self.gui.max_alignment_error,
                                            2)) + " times the maximum allowed, roll back to "
                                            "last alignment point. New time between alignments: " +
                                            str(self.gui.max_seconds_between_autoaligns) +
                                            " seconds.")
                                    # Videos since last auto-alignment have to be repeated.
                                    if len(self.tile_indices_since_last_autoalign) > 0:
                                        self.gui.tv.mark_unprocessed(
                                            self.tile_indices_since_last_autoalign)
                                        # Just in case the currently active tile has just be marked
                                        # unprocessed, set it to active again.
                                        self.gui.tv.mark_active(self.active_tile_number)
                                        # Reset list of tiles since last auto-align (a fresh
                                        # auto-align has been just performed). Save the lowest
                                        # index of the invalidated tiles. When the TileConstructor
                                        # method "find_next_unprocessed_tile" will look for the
                                        # next unprocessed tile, it will start with this one.
                                        self.repeat_from_here = min(
                                            self.tile_indices_since_last_autoalign)
                                        # Reset list of tiles to be repeated.
                                        self.tile_indices_since_last_autoalign = []
                                    else:
                                        self.repeat_from_here = -1
                                else:
                                    # Auto-alignment is accurate enough. Reset list of tiles
                                    # since last
                                    # successful alignment.
                                    self.tile_indices_since_last_autoalign = []
                                    if self.gui.configuration.protocol_level > 0:
                                        Miscellaneous.protocol(
                                            "Auto-alignment accurate: Error is " + str(round(
                                                relative_alignment_error /
                                                self.gui.max_alignment_error,
                                                2)) + " times the maximum allowed.")
                                # If the alignment error was very low, increase time between
                                # auto-alignments (within bounds).
                                if relative_alignment_error < self.gui.max_alignment_error / \
                                        self.gui.configuration.align_very_precise_factor:
                                    self.gui.max_seconds_between_autoaligns = min((
                                            self.gui.max_seconds_between_autoaligns *
                                            self.gui.configuration.align_interval_change_factor),
                                        self.gui.max_autoalign_interval)
                                    if self.gui.configuration.protocol_level > 0:
                                        Miscellaneous.protocol(
                                            "Relative alignment error very small, "
                                            "new time between alignments: " + str(
                                            self.gui.max_seconds_between_autoaligns) + " seconds.")
                            if self.gui.configuration.protocol_level > 0:
                                Miscellaneous.protocol("Auto-alignment successful.")
                        # Auto-alignment was not successful, continue in moon_panorama_maker with
                        # method "wait_for_autoalignment_off" (reset auto-alignment, including gui
                        # button, enable manual alignment button, and prompt user to continue
                        # manually.)
                        except RuntimeError:
                            self.autoalignment_reset_signal.emit()
                            if self.gui.configuration.protocol_level > 0:
                                Miscellaneous.protocol(
                                    "Auto-alignment failed, revert to manual mode.")
                            # No video acquisition because of missing alignment.
                            auto_alignment_disabled = True
                            break
                    if auto_alignment_disabled:
                        continue

                # Alignment is up-to-date, move telescoppe to active tile for video acquisition.
                self.set_text_browser_signal.emit(
                    "Moving telescope to tile " + str(self.active_tile_number) + ", please wait.")
                if self.gui.configuration.protocol_level > 0:
                    print("")
                    Miscellaneous.protocol(
                        "Moving telescope to tile " + str(self.active_tile_number) + ".")
                if self.gui.configuration.protocol_level > 2:
                    Miscellaneous.protocol("RA offset ('): " + str(
                        round(degrees(self.gui.next_tile['delta_ra_center']) * 60.,
                              3)) + ", DE offset ('): " + str(
                        round(degrees(self.gui.next_tile['delta_de_center']) * 60., 3)) + ".")
                (ra, de) = self.al.tile_to_telescope_coordinates(self.gui.next_tile)
                self.telescope.slew_to(ra, de)
                self.set_statusbar_signal.emit()
                # During video acquisition, guide the telescope to follow the moon among stars.
                guiding_rate_ra = self.me.rate_ra
                guiding_rate_de = self.me.rate_de
                # If drift has been determined, include it in guidance rates.
                if self.al.is_drift_set:
                    guiding_rate_ra += self.al.drift_ra
                    guiding_rate_de += self.al.drift_de
                self.telescope.start_guiding(guiding_rate_ra, guiding_rate_de)
                if self.gui.configuration.conf.getboolean("Workflow", "camera automation"):
                    # Wait a little until telescope pointing has stabilized.
                    time.sleep(
                        self.gui.configuration.conf.getfloat("Workflow", "camera trigger delay"))
                    # Send tile number to camera (for inclusion in video file name) and start
                    # camera.
                    self.camera.active_tile_number = self.active_tile_number
                    self.camera.triggered = True
                    if self.gui.configuration.protocol_level > 1:
                        Miscellaneous.protocol("Exposure of tile " + str(
                            self.active_tile_number) + " started automatically.")
                    # If meanwhile the Esc key has been pressed, do not ask for pressing it again.
                    # Otherwise tell the user that he/she can interrupt by pressing 'Exc'.
                    if not self.escape_pressed_flag:
                        self.set_text_browser_signal.emit("Video(s) started automatically. "
                                                          "Press 'Esc' to interrupt loop after "
                                                          "video(s) for current tile.")
                else:
                    # Manual exposure: Set the context in moon_panorama_maker for Enter key.
                    # Pressing it will continue workflow.
                    self.gui.gui_context = "start_continue_recording"
                    self.set_text_browser_signal.emit("Start video(s). After all videos for this "
                                                      " tile are finished, confirm with 'enter'. "
                                                      "Press 'Esc' to interrupt the "
                                                      " recording workflow.")

            # Triggered by method "move_to_selected_tile" in moon_panorama_maker.
            elif self.move_to_selected_tile_flag:
                self.move_to_selected_tile_flag = False
                # First translate tile number into telescope coordinates.
                (ra_selected_tile, de_selected_tile) = (self.al.tile_to_telescope_coordinates(
                    self.tc.list_of_tiles_sorted[self.active_tile_number]))
                # Move telescope to aim point. (This is a blocking operation.)
                self.telescope.slew_to(ra_selected_tile, de_selected_tile)
                self.set_text_browser_signal.emit("")
                # Start method "reset_key_status" in gui to re-activate gui buttons.
                self.reset_key_status_signal.emit()

            # The escape key has been pressed during video workflow. Wait until running activities
            # are safe to be interrupted. Then give control back to gui.
            elif self.escape_pressed_flag:
                self.escape_pressed_flag = False
                # Wait while camera is active.
                if self.gui.configuration.conf.getboolean("Workflow", "camera automation"):
                    while self.camera.active:
                        time.sleep(self.gui.configuration.polling_interval)
                # After video(s) are finished, stop telescope guiding, blank out text browser and
                # give key control back to the user.
                self.telescope.stop_guiding()
                self.set_text_browser_signal.emit("")
                # Start method "reset_key_status" in gui to re-activate gui buttons.
                self.reset_key_status_signal.emit()

            # Sleep time inserted to limit CPU consumption by idle looping.
            time.sleep(self.gui.configuration.polling_interval)  # print ("End of main loop")

        # The "exiting" flag is set (by gui method "CloseEvent"). Terminate the telescope first.
        if self.telescope_connected:
            self.telescope.terminate()
        # If camera automation is active, set termination flag in camera and wait a short while.
        if self.camera_connected:
            if self.gui.configuration.conf.getboolean("Workflow", "camera automation"):
                self.camera.terminate = True
        time.sleep(self.gui.configuration.polling_interval)
        # If stdout was re-directed to a file: Close the file and reset stdout to original value.
        if self.gui.configuration.conf.getboolean('Workflow', 'protocol to file'):
            try:
                self.protocol_file.close()
                # Set standard output back to the value before it was re-routed to protocol file.
                sys.stdout = self.stdout_saved
            except:
                pass
    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