Exemple #1
0
class PssConsole(QtCore.QObject):
    """
    This class replaces the class PlanetarySystemStacker if the program is started from the
    command line. In this case no GUI activity is created, and there is no interactive mode.
    """

    # Define signals which trigger activities on the workflow thread.
    signal_load_master_dark = QtCore.pyqtSignal(str)
    signal_load_master_flat = QtCore.pyqtSignal(str)
    signal_frames = QtCore.pyqtSignal(object)
    signal_rank_frames = QtCore.pyqtSignal(bool)
    signal_align_frames = QtCore.pyqtSignal(int, int, int, int)
    signal_set_roi = QtCore.pyqtSignal(int, int, int, int)
    signal_set_alignment_points = QtCore.pyqtSignal()
    signal_compute_frame_qualities = QtCore.pyqtSignal()
    signal_stack_frames = QtCore.pyqtSignal()
    signal_save_stacked_image = QtCore.pyqtSignal()

    def __init__(self, parent=None):
        super(PssConsole, self).__init__(parent)

        # Create the configuration object and modify it as specified in command line arguments.
        self.setup_configuration()

        # Start the workflow.
        self.work_next_task("Read frames")

    def setup_configuration(self):
        """
        Parse the command line arguments, initialize the configuration object and update
        configuration parameters with values passed via command line arguments.

        :return: -
        """
        parser = ArgumentParser()
        parser.add_argument("job_input",
                            nargs='+',
                            help="input video files or still image folders")

        parser.add_argument("-p",
                            "--protocol",
                            action="store_true",
                            help="Store protocol with results")
        parser.add_argument("--protocol_detail",
                            type=int,
                            choices=[0, 1, 2],
                            default=1,
                            help="Protocol detail level")
        parser.add_argument("-b",
                            "--buffering_level",
                            choices=["auto", "0", "1", "2", "3", "4"],
                            default="auto",
                            help="Buffering level")
        parser.add_argument("--out_format",
                            choices=["png", "tiff", "fits"],
                            default="png",
                            help="Image format for output")
        parser.add_argument(
            "--name_add_f",
            action="store_true",
            help="Add number of stacked frames to output file name")
        parser.add_argument(
            "--name_add_p",
            action="store_true",
            help="Add percentage of stacked frames to output file name")
        parser.add_argument(
            "--name_add_apb",
            action="store_true",
            help="Add alignment point box size (pixels) to output file name")
        parser.add_argument(
            "--name_add_apn",
            action="store_true",
            help="Add number of alignment points to output file name")

        parser.add_argument("--debayering",
                            choices=[
                                "Auto detect color", "Grayscale", "RGB", "RGB",
                                "BGR", "Force Bayer RGGB", "Force Bayer GRBG",
                                "Force Bayer GBRG", "Force Bayer BGGR"
                            ],
                            default="Auto detect color",
                            help="Debayering option")
        parser.add_argument(
            "--debayer_method",
            choices=["Bilinear", "Variable Number of Gradients", "Edge Aware"],
            default="Bilinear",
            help="Debayering method to be used")
        parser.add_argument("--noise",
                            type=noise_type,
                            default=7,
                            help="Noise level (add Gaussian blur)")
        parser.add_argument("-m",
                            "--stab_mode",
                            choices=["Surface", "Planet"],
                            default="Surface",
                            help="Frame stabilization mode")
        parser.add_argument("--stab_size",
                            type=stab_size_type,
                            default=33,
                            help="Stabilization patch size (%% of frame)")
        parser.add_argument("--stab_sw",
                            type=stab_sw_type,
                            default=34,
                            help="Stabilization search width (pixels)")
        parser.add_argument(
            "--rf_percent",
            type=rf_percent_type,
            default=5,
            help="Percentage of best frames for reference frame computation")
        parser.add_argument(
            "--fast_changing_object",
            action="store_true",
            help=
            "The object is changing fast during video time span (e.g. Jupiter")
        parser.add_argument("-d",
                            "--dark",
                            help="Image file for dark frame correction")
        parser.add_argument("-f",
                            "--flat",
                            help="Image file for flat frame correction")

        parser.add_argument("-a",
                            "--align_box_width",
                            type=align_box_width_type,
                            default=48,
                            help="Alignment point box width (pixels)")
        parser.add_argument("-w",
                            "--align_search_width",
                            type=align_search_width_type,
                            default=14,
                            help="Alignment point search width (pixels)")
        parser.add_argument("--align_min_struct",
                            type=align_min_struct_type,
                            default=0.04,
                            help="Alignment point minimum structure")
        parser.add_argument("--align_min_bright",
                            type=align_min_bright_type,
                            default=10,
                            help="Alignment point minimum brightness")

        parser.add_argument("-s",
                            "--stack_percent",
                            type=stack_percent_type,
                            default=10,
                            help="Percentage of best frames to be stacked")
        parser.add_argument("--stack_number",
                            type=stack_number_type,
                            help="Number of best frames to be stacked")
        parser.add_argument("-n",
                            "--normalize_bright",
                            action="store_true",
                            help="Normalize frame brightness")
        parser.add_argument("--normalize_bco",
                            type=normalize_bco_type,
                            default=15,
                            help="Normalization black cut-off")
        parser.add_argument("--drizzle",
                            choices=["Off", "1.5x", "2x", "3x"],
                            default="Off",
                            help="Drizzle factor (Off, 1.5x, 2x, 3x)")

        arguments = parser.parse_args()
        # self.print_arguments(arguments)

        # Create and initialize the configuration object. The configuration stored in the .ini file
        # in the user's home directory is ignored in this case. Modifications to standard values
        # come as command line arguments.
        self.configuration = Configuration()
        self.configuration.initialize_configuration(read_from_file=False)

        # Modify the standard configuration as specified in the command line arguments.
        self.configuration.global_parameters_store_protocol_with_result = arguments.protocol
        self.configuration.global_parameters_protocol_level = arguments.protocol_detail
        if arguments.buffering_level == "auto":
            self.configuration.global_parameters_buffering_level = -1
        else:
            self.configuration.global_parameters_buffering_level = int(
                arguments.buffering_level)
        self.configuration.global_parameters_image_format = arguments.out_format
        self.configuration.global_parameters_parameters_in_filename = arguments.name_add_f or \
            arguments.name_add_p or arguments.name_add_apb or arguments.name_add_apn
        self.configuration.global_parameters_stack_number_frames = arguments.name_add_f
        self.configuration.global_parameters_stack_percent_frames = arguments.name_add_p
        self.configuration.global_parameters_ap_box_size = arguments.name_add_apb
        self.configuration.global_parameters_ap_number = arguments.name_add_apn

        self.configuration.frames_debayering_default = arguments.debayering
        self.configuration.frames_debayering_method = arguments.debayer_method
        self.configuration.frames_gauss_width = arguments.noise

        self.configuration.align_frames_mode = arguments.stab_mode
        self.configuration.align_frames_rectangle_scale_factor = 100. / arguments.stab_size
        self.configuration.align_frames_search_width = arguments.stab_sw
        self.configuration.align_frames_average_frame_percent = arguments.rf_percent
        self.configuration.align_frames_fast_changing_object = arguments.fast_changing_object

        self.configuration.alignment_points_half_box_width = int(
            round(arguments.align_box_width / 2))
        self.configuration.alignment_points_search_width = arguments.align_search_width
        self.configuration.alignment_points_structure_threshold = arguments.align_min_struct
        self.configuration.alignment_points_brightness_threshold = arguments.align_min_bright

        self.configuration.alignment_points_frame_percent = arguments.stack_percent
        self.configuration.alignment_points_frame_number = -1

        # If the number of frames to be stacked is given, it has precedence over the percentage.
        if arguments.stack_number is not None:
            self.configuration.alignment_points_frame_number = arguments.stack_number
            self.configuration.alignment_points_frame_percent = -1

        self.configuration.frames_normalization = arguments.normalize_bright
        self.configuration.frames_normalization_threshold = arguments.normalize_bco
        self.configuration.stack_frames_drizzle_factor_string = arguments.drizzle

        # Re-compute derived parameters after the configuration was changed.
        self.configuration.set_derived_parameters()

        # Create the workflow thread and start it.
        self.thread = QtCore.QThread()
        self.workflow = Workflow(self)
        self.workflow.setParent(None)
        self.workflow.moveToThread(self.thread)
        self.workflow.calibration.report_calibration_error_signal.connect(
            self.report_calibration_error)
        self.workflow.work_next_task_signal.connect(self.work_next_task)
        self.workflow.report_error_signal.connect(self.report_error)
        self.workflow.abort_job_signal.connect(self.next_job_after_error)
        self.thread.start()

        # Connect signals to start activities on the workflow thread (e.g. in method
        # "work_next_task").
        self.signal_load_master_dark.connect(
            self.workflow.calibration.load_master_dark)
        self.signal_load_master_flat.connect(
            self.workflow.calibration.load_master_flat)
        self.signal_frames.connect(self.workflow.execute_frames)
        self.signal_rank_frames.connect(self.workflow.execute_rank_frames)
        self.signal_align_frames.connect(self.workflow.execute_align_frames)
        self.signal_set_roi.connect(self.workflow.execute_set_roi)
        self.signal_set_alignment_points.connect(
            self.workflow.execute_set_alignment_points)
        self.signal_compute_frame_qualities.connect(
            self.workflow.execute_compute_frame_qualities)
        self.signal_stack_frames.connect(self.workflow.execute_stack_frames)
        self.signal_save_stacked_image.connect(
            self.workflow.execute_save_stacked_image)

        # Set "automatic" to True. There is no interactive mode in this case.
        self.automatic = True

        # Create the job objects using the names passed as positional arguments.
        self.jobs = []
        for name in [f for name in arguments.job_input for f in glob(name)]:
            try:
                job = Job(name)

                # Test if the path specifies a stacking job.
                if job.type == 'video' or job.type == 'image':
                    self.jobs.append(job)
                else:
                    if self.configuration.global_parameters_protocol_level > 0:
                        Miscellaneous.protocol(
                            "Error: '" + name +
                            "' does not contain valid input for a stacking job,"
                            " continune with next job.\n",
                            self.workflow.attached_log_file)
            except InternalError:
                if self.configuration.global_parameters_protocol_level > 0:
                    Miscellaneous.protocol(
                        "Error: '" + name +
                        "' does not contain valid input for a stacking job,"
                        " continune with next job.\n",
                        self.workflow.attached_log_file)

        self.job_number = len(self.jobs)
        if self.job_number == 0:
            if self.configuration.global_parameters_protocol_level > 0:
                Miscellaneous.protocol(
                    "Error: No valid job specified, execution halted.",
                    self.workflow.attached_log_file)
            self.stop_execution()

        self.job_index = 0

        # If a dark frame was specified, load it.
        if arguments.dark:
            if self.configuration.global_parameters_protocol_level > 0:
                Miscellaneous.protocol("+++ Loading master dark frame +++",
                                       self.workflow.attached_log_file)
            self.signal_load_master_dark.emit(arguments.dark)

        # If a flat frame was specified, load it.
        if arguments.flat:
            if self.configuration.global_parameters_protocol_level > 0:
                Miscellaneous.protocol("+++ Loading master flat frame +++",
                                       self.workflow.attached_log_file)
            self.signal_load_master_flat.emit(arguments.flat)

    @QtCore.pyqtSlot(str)
    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)

    @QtCore.pyqtSlot(str)
    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)

    @QtCore.pyqtSlot(str)
    def next_job_after_error(self, message):
        """
        This method is triggered by the workflow thread via a signal when an error causes a job to
        be aborted. Depending on the protocol level, the error message is written to the
        protocol (file).

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

        # Report the error.
        self.report_error(message)

        # Abort the current job and go to the next one.
        self.work_next_task("Next job")

    def work_next_task(self, next_activity):
        """
        This is the central place where all activities are scheduled. Depending on the
         "next_activity" chosen, the appropriate activity is started on the workflow thread.

        :param next_activity: Activity to be performed next.
        :return: -
        """

        # Make sure not to process an empty job list, or a job index out of range.
        if not self.jobs or self.job_index >= self.job_number:
            return

        self.activity = next_activity

        # Start workflow activities. When a workflow method terminates, it invokes this method on
        # the GUI thread, with "next_activity" denoting the next step in the processing chain.
        if self.activity == "Read frames":

            # For the first activity (reading all frames from the file system) there is no
            # GUI interaction. Start the workflow action immediately.
            self.signal_frames.emit(self.jobs[self.job_index])

        elif self.activity == "Rank frames":

            # In batch mode no frames can be dropped in the user dialog.
            update_index_translation_table = False
            # Now start the corresponding action on the workflow thread.
            self.signal_rank_frames.emit(update_index_translation_table)

        elif self.activity == "Select frames":

            # The dialog to exclude frames is not to be called. Go to frames alignment
            # immediately.
            self.signal_align_frames.emit(0, 0, 0, 0)

        elif self.activity == "Select stack size":

            # In automatic mode, nothing is to be done in the workflow thread. Start the next
            # activity on the main thread immediately.
            self.workflow.work_next_task_signal.emit("Set ROI")

        elif self.activity == "Set ROI":

            # If all index bounds are set to zero, no ROI is selected.
            self.signal_set_roi.emit(0, 0, 0, 0)

        elif self.activity == "Set alignment points":

            # In automatic mode, compute the AP grid automatically in the workflow thread. In this
            # case, the AlignmentPoints object is created there as well.
            self.signal_set_alignment_points.emit()

        elif self.activity == "Compute frame qualities":
            self.signal_compute_frame_qualities.emit()

        elif self.activity == "Stack frames":
            self.signal_stack_frames.emit()

        elif self.activity == "Save stacked image":
            self.signal_save_stacked_image.emit()

        elif self.activity == "Next job":
            self.job_index += 1
            if self.job_index < self.job_number:
                # If the end of the queue is not reached yet, start with reading frames of next job.
                self.activity = "Read frames"
                self.signal_frames.emit(self.jobs[self.job_index])
            else:
                self.stop_execution()

    def print_arguments(self, arguments):
        """
        This is an auxiliary method for debugging. It prints all arguments passed to the program.

        :param arguments: Arguments object created by the ArgumentParser
        :return: -
        """

        print("Jobs: " + str(arguments.job_input))
        print("Store protocol with results: " + str(arguments.protocol))
        print("Protocol detail level: " + str(arguments.protocol_detail))
        print("Buffering level: " + str(arguments.buffering_level))
        print("Image format for output: " + arguments.out_format)
        print("Add number of stacked frames to output file name: " +
              str(arguments.name_add_f))
        print("Add percentage of stacked frames to output file name: " +
              str(arguments.name_add_p))
        print("Add alignment point box size (pixels) to output file name: " +
              str(arguments.name_add_apb))
        print("Add number of alignment points to output file name: " +
              str(arguments.name_add_apn))
        print("")
        print("Debayering option: " + arguments.debayering)
        print("Debayering method: " + arguments.debayer_method)
        print("Noise level: " + str(arguments.noise))
        print("Frame stabilization mode: " + arguments.stab_mode)
        print("Stabilization patch size (% of frame): " +
              str(arguments.stab_size))
        print("Stabilization search width (pixels): " + str(arguments.stab_sw))
        print("Percentage of best frames for reference frame computation: " +
              str(arguments.rf_percent))
        if arguments.dark:
            print("Image file for dark frame correction: " + arguments.dark)
        if arguments.flat:
            print("Image file for flat frame correction: " + arguments.flat)
        print("")
        print("Alignment point box width (pixels): " +
              str(arguments.align_box_width))
        print("Alignment point search width (pixels): " +
              str(arguments.align_search_width))
        print("Alignment point minimum structure: " +
              str(arguments.align_min_struct))
        print("Alignment point minimum brightness: " +
              str(arguments.align_min_bright))
        print("")
        print("Percentage of best frames to be stacked: " +
              str(arguments.stack_percent))
        print("Number of best frames to be stacked: " +
              str(arguments.stack_number))
        print("Normalize frame brightness: " + str(arguments.normalize_bright))
        print("Normalization black cut-off: " + str(arguments.normalize_bco))

    def stop_execution(self):
        """
        Halt the application. (There might be a more elegant way to do this!)

        :return: -
        """
        # Wait a little before exiting, so that output buffers can be purged.
        sleep(0.2)
        quit(0)