Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate coarse autofocus from fine autofocus #659

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 14 additions & 16 deletions pocs/camera/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,43 +300,41 @@ def autofocus(self,
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.
take_dark (bool, optional): If True will attempt to take a dark frame
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.
plots (bool, optional: Whether to write focus plots to images folder, default False.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would prefer to see this called make_plots but we could make a separate issue to change things like that all at once.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Yes, there are probably quite a few of these sorts of terminology inconsistencies to be fixed.

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!")
Expand Down
66 changes: 19 additions & 47 deletions pocs/focuser/focuser.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,8 @@ def autofocus(self,
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
Expand All @@ -209,7 +206,8 @@ 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.
coarse (bool, optional): Whether to perform a coarse focus, otherwise will perform
a fine focus. 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.

Expand Down Expand Up @@ -284,6 +282,7 @@ def autofocus(self,
mask_dilations = 10

# Set up the focus parameters
focus_event = Event()
focus_params = {
'seconds': seconds,
'focus_range': focus_range,
Expand All @@ -294,35 +293,16 @@ def autofocus(self,
'merit_function': merit_function,
'merit_function_kwargs': merit_function_kwargs,
'mask_dilations': mask_dilations,
'coarse': coarse,
'plots': plots,
'start_event': None,
'finished_event': None,
'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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should include some ultimate timeout in the wait

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Needs to be a rather conservative one though, as it's a bit involved to predict exactly how long it will take.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might be a job for another PR actually. Want to add timeouts to the camera's threaded operations more generally to prevent the lock ups we sometimes get.


return fine_event
return focus_event

def _autofocus(self,
seconds,
Expand All @@ -336,20 +316,13 @@ def _autofocus(self,
mask_dilations,
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'
Expand Down Expand Up @@ -490,14 +463,13 @@ def _autofocus(self,

final_focus = self.move_to(best_focus)

final_fn = "{}_{}.{}".format(final_focus, "final", self._camera.file_extension)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this include the focus_type? I don't seem to see it set anywhere.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good idea. It should. I'll put that in.

file_path = os.path.join(file_path_root, final_fn)
final_thumbnail = self._camera.get_thumbnail(
seconds, file_path, thumbnail_size, keep_file=True)

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_thumbnail = self._camera.get_thumbnail(
seconds, file_path, thumbnail_size, keep_file=True)
final_thumbnail = focus_utils.mask_saturated(final_thumbnail)
if dark_thumb is not None:
initial_thumbnail = initial_thumbnail - dark_thumb
Expand Down Expand Up @@ -552,8 +524,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

Expand Down
51 changes: 44 additions & 7 deletions pocs/tests/test_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(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, 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):
Expand Down