diff --git a/pocs/camera/camera.py b/pocs/camera/camera.py index 38b574f3d..0dc64c02c 100644 --- a/pocs/camera/camera.py +++ b/pocs/camera/camera.py @@ -296,26 +296,23 @@ def autofocus(self, merit_function_kwargs={}, mask_dilations=None, coarse=False, - plots=False, + make_plots=False, blocking=False, *args, **kwargs): """ - Focuses the camera using the specified merit function. Optionally - performs a coarse focus first before performing the default fine focus. - The expectation is that coarse focus will only be required for first use - of a optic to establish the approximate position of infinity focus and - after updating the intial focus position in the config only fine focus - will be required. + Focuses the camera using the specified merit function. Optionally performs + a coarse focus to find the approximate position of infinity focus, which + should be followed by a fine focus before observing. Args: - seconds (optional): Exposure time for focus exposures, if not + seconds (scalar, optional): Exposure time for focus exposures, if not specified will use value from config. focus_range (2-tuple, optional): Coarse & fine focus sweep range, in encoder units. Specify to override values from config. focus_step (2-tuple, optional): Coarse & fine focus sweep steps, in encoder units. Specify to override values from config. - thumbnail_size (optional): Size of square central region of image to - use, default 500 x 500 pixels. + thumbnail_size (int, optional): Size of square central region of image + to use, default 500 x 500 pixels. keep_files (bool, optional): If True will keep all images taken during focusing. If False (default) will delete all except the first and last images from each focus run. @@ -323,20 +320,22 @@ def autofocus(self, before the focus run, and use it for dark subtraction and hot pixel masking, default True. merit_function (str/callable, optional): Merit function to use as a - focus metric. + focus metric, default vollath_F4. merit_function_kwargs (dict, optional): Dictionary of additional keyword arguments for the merit function. mask_dilations (int, optional): Number of iterations of dilation to perform on the saturated pixel mask (determine size of masked regions), default 10 - coarse (bool, optional): Whether to begin with coarse focusing, - default False - plots (bool, optional: Whether to write focus plots to images folder, - default False. - blocking (bool, optional): Whether to block until autofocus complete, - default False + coarse (bool, optional): Whether to perform a coarse focus, otherwise will perform + a fine focus. Default False. + make_plots (bool, optional: Whether to write focus plots to images folder, default + False. + blocking (bool, optional): Whether to block until autofocus complete, default False. Returns: threading.Event: Event that will be set when autofocusing is complete + + Raises: + ValueError: If invalid values are passed for any of the focus parameters. """ if self.focuser is None: self.logger.error("Camera must have a focuser for autofocus!") @@ -352,7 +351,7 @@ def autofocus(self, merit_function_kwargs=merit_function_kwargs, mask_dilations=mask_dilations, coarse=coarse, - plots=plots, + make_plots=make_plots, blocking=blocking, *args, **kwargs) diff --git a/pocs/focuser/focuser.py b/pocs/focuser/focuser.py index 544971572..218af2cad 100644 --- a/pocs/focuser/focuser.py +++ b/pocs/focuser/focuser.py @@ -178,15 +178,12 @@ def autofocus(self, merit_function_kwargs=None, mask_dilations=None, coarse=False, - plots=False, + make_plots=False, blocking=False): """ Focuses the camera using the specified merit function. Optionally performs - a coarse focus first before performing the default fine focus. The - expectation is that coarse focus will only be required for first use - of a optic to establish the approximate position of infinity focus and - after updating the intial focus position in the config only fine focus will - be required. + a coarse focus to find the approximate position of infinity focus, which + should be followed by a fine focus before observing. Args: seconds (scalar, optional): Exposure time for focus exposures, if not @@ -209,8 +206,10 @@ def autofocus(self, keyword arguments for the merit function. mask_dilations (int, optional): Number of iterations of dilation to perform on the saturated pixel mask (determine size of masked regions), default 10 - coarse (bool, optional): Whether to begin with coarse focusing, default False. - plots (bool, optional: Whether to write focus plots to images folder, default False. + coarse (bool, optional): Whether to perform a coarse focus, otherwise will perform + a fine focus. Default False. + make_plots (bool, optional: Whether to write focus plots to images folder, default + False. blocking (bool, optional): Whether to block until autofocus complete, default False. Returns: @@ -284,6 +283,7 @@ def autofocus(self, mask_dilations = 10 # Set up the focus parameters + focus_event = Event() focus_params = { 'seconds': seconds, 'focus_range': focus_range, @@ -294,35 +294,16 @@ def autofocus(self, 'merit_function': merit_function, 'merit_function_kwargs': merit_function_kwargs, 'mask_dilations': mask_dilations, - 'plots': plots, - 'start_event': None, - 'finished_event': None, + 'coarse': coarse, + 'make_plots': make_plots, + 'focus_event': focus_event, } - - # Coarse focus - if coarse: - coarse_event = Event() - focus_params['finished_event'] = coarse_event - focus_params['coarse'] = True - - coarse_thread = Thread(target=self._autofocus, kwargs=focus_params) - coarse_thread.start() - else: - coarse_event = None - - # Fine Focus - This will wait for the coarse_event to finish. - fine_event = Event() - focus_params['start_event'] = coarse_event - focus_params['finished_event'] = fine_event - focus_params['coarse'] = False - - fine_thread = Thread(target=self._autofocus, kwargs=focus_params) - fine_thread.start() - + focus_thread = Thread(target=self._autofocus, kwargs=focus_params) + focus_thread.start() if blocking: - fine_event.wait() + focus_event.wait() - return fine_event + return focus_event def _autofocus(self, seconds, @@ -334,22 +315,15 @@ def _autofocus(self, merit_function, merit_function_kwargs, mask_dilations, - plots, + make_plots, coarse, - start_event, - finished_event, + focus_event, *args, **kwargs): """Private helper method for calling autofocus in a Thread. See public `autofocus` for information about the parameters. """ - - # If passed a start_event wait until Event is set before proceeding - # (e.g. wait for coarse focus to finish before starting fine focus). - if start_event: - start_event.wait() - focus_type = 'fine' if coarse: focus_type = 'coarse' @@ -383,7 +357,10 @@ def _autofocus(self, self.logger.warning("Camera {} does not support dark frames!".format(self._camera)) # Take an image before focusing, grab a thumbnail from the centre and add it to the plot - initial_fn = "{}_{}.{}".format(initial_focus, "initial", self._camera.file_extension) + initial_fn = "{}_{}_{}.{}".format(initial_focus, + focus_type, + "initial", + self._camera.file_extension) initial_path = os.path.join(file_path_root, initial_fn) initial_thumbnail = self._camera.get_thumbnail( @@ -490,14 +467,16 @@ def _autofocus(self, final_focus = self.move_to(best_focus) - if plots: - initial_thumbnail = focus_utils.mask_saturated(initial_thumbnail) - - final_fn = "{}_{}.{}".format(final_focus, "final", self._camera.file_extension) - file_path = os.path.join(file_path_root, final_fn) + final_fn = "{}_{}_{}.{}".format(final_focus, + focus_type, + "final", + self._camera.file_extension) + file_path = os.path.join(file_path_root, final_fn) + final_thumbnail = self._camera.get_thumbnail( + seconds, file_path, thumbnail_size, keep_file=True) - final_thumbnail = self._camera.get_thumbnail( - seconds, file_path, thumbnail_size, keep_file=True) + if make_plots: + initial_thumbnail = focus_utils.mask_saturated(initial_thumbnail) final_thumbnail = focus_utils.mask_saturated(final_thumbnail) if dark_thumb is not None: initial_thumbnail = initial_thumbnail - dark_thumb @@ -552,8 +531,8 @@ def _autofocus(self, self.logger.debug( 'Autofocus of {} complete - final focus position: {}', self._camera, final_focus) - if finished_event: - finished_event.set() + if focus_event: + focus_event.set() return initial_focus, final_focus diff --git a/pocs/tests/test_camera.py b/pocs/tests/test_camera.py index 898e1504b..ea7fb0ea4 100644 --- a/pocs/tests/test_camera.py +++ b/pocs/tests/test_camera.py @@ -10,6 +10,7 @@ from pocs.utils.config import load_config from pocs.utils.error import NotFound +import glob import os import time from ctypes.util import find_library @@ -64,9 +65,25 @@ def camera(request, images_dir): camera.config['directories']['images'] = images_dir return camera -# Hardware independent tests, mostly use simulator: + +@pytest.fixture(scope='module') +def counter(): + return {'value': 0} + + +@pytest.fixture(scope='module') +def patterns(camera, images_dir): + patterns = {'final': os.path.join(images_dir, 'focus', camera.uid, '*', + ('*_final.' + camera.file_extension)), + 'fine_plot': os.path.join(images_dir, 'focus', camera.uid, '*', + 'fine_focus.png'), + 'coarse_plot': os.path.join(images_dir, 'focus', camera.uid, '*', + 'coarse_focus.png')} + return patterns +# Hardware independent tests, mostly use simulator: + def test_sim_create_focuser(): sim_camera = SimCamera(focuser={'model': 'simulator', 'focus_port': '/dev/ttyFAKE'}) assert isinstance(sim_camera.focuser, Focuser) @@ -289,29 +306,49 @@ def test_observation(camera): time.sleep(7) -def test_autofocus_coarse(camera): +def test_autofocus_coarse(camera, patterns, counter): autofocus_event = camera.autofocus(coarse=True) autofocus_event.wait() + counter['value'] += 1 + assert len(glob.glob(patterns['final'])) == counter['value'] -def test_autofocus_fine(camera): +def test_autofocus_fine(camera, patterns, counter): autofocus_event = camera.autofocus() autofocus_event.wait() + counter['value'] += 1 + assert len(glob.glob(patterns['final'])) == counter['value'] -def test_autofocus_fine_blocking(camera): +def test_autofocus_fine_blocking(camera, patterns, counter): autofocus_event = camera.autofocus(blocking=True) assert autofocus_event.is_set() + counter['value'] += 1 + assert len(glob.glob(patterns['final'])) == counter['value'] + + +def test_autofocus_with_plots(camera, patterns, counter): + autofocus_event = camera.autofocus(make_plots=True) + autofocus_event.wait() + counter['value'] += 1 + assert len(glob.glob(patterns['final'])) == counter['value'] + assert len(glob.glob(patterns['fine_plot'])) == 1 -def test_autofocus_no_plots(camera): - autofocus_event = camera.autofocus(plots=False) +def test_autofocus_coarse_with_plots(camera, patterns, counter): + autofocus_event = camera.autofocus(coarse=True, make_plots=True) autofocus_event.wait() + counter['value'] += 1 + assert len(glob.glob(patterns['final'])) == counter['value'] + assert len(glob.glob(patterns['fine_plot'])) == 1 + assert len(glob.glob(patterns['coarse_plot'])) == 1 -def test_autofocus_keep_files(camera): +def test_autofocus_keep_files(camera, patterns, counter): autofocus_event = camera.autofocus(keep_files=True) autofocus_event.wait() + counter['value'] += 1 + assert len(glob.glob(patterns['final'])) == counter['value'] def test_autofocus_no_size(camera):