From 98552a23f202b7c31256616215d5c31aaf870708 Mon Sep 17 00:00:00 2001 From: Vincent Berenz Date: Thu, 12 Dec 2024 16:51:51 +0100 Subject: [PATCH] added focus executable, not tested --- nightskycam_focus/adapter.py | 34 +++++++++- nightskycam_focus/main.py | 117 ++++++++++++++++++++++++++++++----- pyproject.toml | 3 +- 3 files changed, 135 insertions(+), 19 deletions(-) diff --git a/nightskycam_focus/adapter.py b/nightskycam_focus/adapter.py index 536e532..754e93b 100644 --- a/nightskycam_focus/adapter.py +++ b/nightskycam_focus/adapter.py @@ -25,6 +25,31 @@ class CommandType(Enum): APERTURE = "A" +class Aperture(Enum): + MAX = 441 + V0 = 441 + V1 = 512 + V2 = 646 + V3 = 706 + V4 = 857 + V5 = 926 + V6 = 1110 + V7 = 1159 + V8 = 1271 + V9 = 1347 + V10 = 1468 + V11 = 2303 + MIN = 2303 + + @classmethod + def is_valid(cls, aperture: str) -> bool: + return aperture in cls.__members__ + + @classmethod + def get(cls, aperture: str) -> "Aperture": + return cls.__members__[aperture] + + PIN = NewType("PIN", int) SS_PIN = PIN(5) RESET_PIN = PIN(6) @@ -79,7 +104,9 @@ def _prepare_message(command: int, v1: int, v2: int) -> List[int]: _ERROR_RESET = (2, 2, 2, 2) -def _spi_send(spi: spidev.SpiDev, command_type: CommandType, value: int) -> None: +def _spi_send( + spi: spidev.SpiDev, command_type: CommandType, value: int +) -> None: logging.debug(f"command {command_type}: {value}") v1, v2 = divmod(value, 256) command = ord(command_type.value) @@ -163,5 +190,6 @@ def set_focus(target_value: int) -> None: _send_command(CommandType.FOCUS, target_value) -def set_aperture(target_value: int) -> None: - _send_command(CommandType.APERTURE, target_value) +def set_aperture(target_value: Aperture) -> None: + value: int = target_value.value + _send_command(CommandType.APERTURE, value) diff --git a/nightskycam_focus/main.py b/nightskycam_focus/main.py index 2f283c0..8e65f5a 100644 --- a/nightskycam_focus/main.py +++ b/nightskycam_focus/main.py @@ -1,3 +1,4 @@ +from pathlib import Path import argparse import logging import sys @@ -7,12 +8,15 @@ import numpy as np from camera_zwo_asi import ImageType from camera_zwo_asi.camera import Camera +from camera_zwo_asi.image import Image -from .adapter import adapter +from .adapter import adapter, set_focus, set_aperture, Aperture from .focus import find_focus -def _get_pixel_from_user(image: np.ndarray, resize: int = 4) -> Tuple[int, int]: +def _get_pixel_from_user( + image: np.ndarray, resize: int = 4 +) -> Tuple[int, int]: down_sized_image = cv2.resize( image, (image.shape[1] // resize, image.shape[0] // resize) ) @@ -39,7 +43,7 @@ def _click_event(event, x, y, flags, param): return full_size_pixel -def _get_full_image(exposure: int, gain: int, camera_index: int) -> np.ndarray: +def _capture(exposure: int, gain: int, camera_index: int) -> Image: camera = Camera(camera_index) camera.set_control("Exposure", exposure) camera.set_control("Gain", gain) @@ -47,7 +51,12 @@ def _get_full_image(exposure: int, gain: int, camera_index: int) -> np.ndarray: roi.bins = 1 roi.type = ImageType.rgb24 camera.set_roi(roi) - return camera.capture().get_image() + return camera.capture() + + +def _get_full_image(exposure: int, gain: int, camera_index: int) -> np.ndarray: + image = _capture(exposure, gain, camera_index) + return image.get_image() def _add_border(img: np.array, thickness: int = 5, color=(255, 0, 0)): @@ -68,9 +77,11 @@ def _add_border(img: np.array, thickness: int = 5, color=(255, 0, 0)): return bordered_img -def _zwo_asi_focus(): +def _zwo_asi_focus_sweep(): - parser = argparse.ArgumentParser(description="Focus sweep on zwo-asi camera") + parser = argparse.ArgumentParser( + description="Focus sweep on zwo-asi camera" + ) parser.add_argument( "--camera_index", type=int, @@ -91,10 +102,16 @@ def _zwo_asi_focus(): help="Camera exposure (default: %(default)s)", ) parser.add_argument( - "--gain", type=int, default=121, help="Camera gain (default: %(default)s)" + "--gain", + type=int, + default=121, + help="Camera gain (default: %(default)s)", ) parser.add_argument( - "--step", type=int, default=20, help="Focus step size (default: %(default)s)" + "--step", + type=int, + default=20, + help="Focus step size (default: %(default)s)", ) parser.add_argument("--show", action="store_true", help="Show the image") parser.add_argument( @@ -115,16 +132,22 @@ def _zwo_asi_focus(): # setting the logs if args.verbose: - logging.basicConfig(level=logging.DEBUG, format="focus sweep: %(message)s") + logging.basicConfig( + level=logging.DEBUG, format="focus sweep: %(message)s" + ) elif args.silent: ... else: - logging.basicConfig(level=logging.INFO, format="focus sweep: %(message)s") + logging.basicConfig( + level=logging.INFO, format="focus sweep: %(message)s" + ) - logging.info(f"capturing image (exposure: {args.exposure}, gain: {args.gain})") + logging.info( + f"capturing image (exposure: {args.exposure}, gain: {args.gain})" + ) full_image = _get_full_image(args.exposure, args.gain, args.camera_index) - logging.info(f"prompting user for target pixel") + logging.info("prompting user for target pixel") pixel = _get_pixel_from_user(full_image) logging.info( @@ -159,9 +182,9 @@ def _zwo_asi_focus(): cv2.imwrite("focus.png", concatenated_images) -def zwo_asi_focus(): +def zwo_asi_focus_sweep(): try: - _zwo_asi_focus() + _zwo_asi_focus_sweep() sys.exit(0) except Exception as e: print(f"Error: {e}", file=sys.stderr) @@ -169,6 +192,70 @@ def zwo_asi_focus(): def zwo_asi_focus_test(): - logging.basicConfig(level=logging.DEBUG, format="focus sweep: %(message)s") + logging.basicConfig(level=logging.DEBUG, format="focus test: %(message)s") with adapter(): logging.info("adapter running") + + +def _check_range(value: int, minimum: int = 350, maximum: int = 650) -> int: + """Check if the input value is an integer within the range 350 to 650.""" + try: + ivalue = int(value) + except ValueError: + raise argparse.ArgumentTypeError(f"{value} is not a valid integer") + if ivalue < minimum or ivalue > maximum: + raise argparse.ArgumentTypeError( + f"{value} is an invalid value. Must be between {minimum} and {maximum}." + ) + return ivalue + + +def _valid_aperture(value: str) -> Aperture: + try: + return Aperture.get(value) + except KeyError: + raise argparse.ArgumentTypeError( + f"{value} is not a valid aperture. Valid apertures: MAX (open), V1, ..., V10, MIN (closed)" + ) + + +def zwo_asi_focus(): + logging.basicConfig(level=logging.DEBUG, format="focus: %(message)s") + parser = argparse.ArgumentParser( + description="Focus change on zwo-asi camera" + ) + parser.add_argument( + "focus", + type=_check_range, + help="desired focus. int between 350 and 650", + ) + parser.add_argument( + "--aperture", + type=_valid_aperture, + help="desired aperture. MAX: open, MIN: close, V1 ... V10: intermediate values", + required=False, + ) + parser.add_argument( + "--exposure", + type=int, + help="if set (microseconds), a picture will be taken and saved in the current directory", + required=False, + ) + args = parser.parse_args() + with adapter(): + logging.info(f"setting focus to {args.focus}") + set_focus(args.focus) + if args.aperture is not None: + logging.info(f"setting aperture to {args.aperture}") + set_aperture(args.aperture) + if args.exposure is None: + return + logging.info(f"taking picture with exposure {args.exposure}") + image = _capture(args.exposure, 121, 0) + if args.aperture: + filename = f"img_{args.focus}_{args.aperture}_{args.exposure}.tiff" + else: + filename = f"img_{args.focus}_{args.exposure}.tiff" + filepath = str(Path.cwd() / filename) + logging.info(f"saving image to {filepath}") + image.save(filepath) diff --git a/pyproject.toml b/pyproject.toml index aab7b2b..a1ce035 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,8 +6,9 @@ authors = ["Vincent Berenz "] packages = [{ include = "nightskycam_focus" }] [tool.poetry.scripts] -zwo-asi-focus = 'nightskycam_focus.main:zwo_asi_focus' +zwo-asi-focus-sweep = 'nightskycam_focus.main:zwo_asi_focus_sweep' zwo-asi-focus-test = 'nightskycam_focus.main:zwo_asi_focus_test' +zwo-asi-focus = 'nightskycam_focus.main:zwo_asi_focus' [tool.poetry.dependencies] python = "^3.9"