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)