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

Move camera configurations close to camera classes #74

Merged
merged 2 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 2 additions & 3 deletions modules/camera/base_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

import numpy as np

from . import camera_configurations


class BaseCameraDevice(abc.ABC):
"""
Expand All @@ -20,13 +18,14 @@ def create(
cls,
width: int,
height: int,
config: camera_configurations.PiCameraConfig | camera_configurations.OpenCVCameraConfig,
config: object,
) -> "tuple[True, BaseCameraDevice] | tuple[False, None]":
"""
Abstract create method.

width: Width of the camera.
height: Height of the camera.
config: Configuration.

Return: Success, camera object.
"""
Expand Down
62 changes: 0 additions & 62 deletions modules/camera/camera_configurations.py

This file was deleted.

3 changes: 1 addition & 2 deletions modules/camera/camera_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import enum

from . import base_camera
from . import camera_configurations
from . import camera_opencv
from . import camera_picamera2

Expand All @@ -23,7 +22,7 @@ def create_camera(
camera_option: CameraOption,
width: int,
height: int,
config: camera_configurations.PiCameraConfig | camera_configurations.OpenCVCameraConfig,
config: camera_opencv.ConfigOpenCV | camera_picamera2.ConfigPiCamera2,
) -> tuple[True, base_camera.BaseCameraDevice] | tuple[False, None]:
"""
Create a camera object based off of given parameters.
Expand Down
26 changes: 20 additions & 6 deletions modules/camera/camera_opencv.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,18 @@
import numpy as np

from . import base_camera
from . import camera_configurations


class ConfigOpenCV:
"""
Configuration for the OpenCV camera.
"""

def __init__(self, device_index: int) -> None:
"""
index: Index of the device.
"""
self.device_index = device_index


class CameraOpenCV(base_camera.BaseCameraDevice):
Expand All @@ -18,21 +29,24 @@ class CameraOpenCV(base_camera.BaseCameraDevice):

@classmethod
def create(
cls, width: int, height: int, config: camera_configurations.OpenCVCameraConfig = None
cls, width: int, height: int, config: ConfigOpenCV
) -> "tuple[True, CameraOpenCV] | tuple[False, None]":
"""
OpenCV Camera.

width: Width of the camera.
height: Height of the camera.
config (OpenCVCameraConfig, optional): Configuration of the camera.
config: Configuration of for OpenCV camera.

Return: Success, camera object.
"""
if width <= 0:
return False, None

if height <= 0:
return False, None

# TODO: apply camera configs to camera here
_ = config # placeholder
camera = cv2.VideoCapture(0)
camera = cv2.VideoCapture(config.device_index)
if not camera.isOpened():
return False, None

Expand Down
82 changes: 69 additions & 13 deletions modules/camera/camera_picamera2.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,60 @@

# Picamera2 library only exists on Raspberry Pi
try:
import libcamera
import picamera2
except ImportError:
pass

from . import base_camera
from . import camera_configurations


# TODO: pass in as constructor parameter
CAMERA_TIMEOUT = 1
class ConfigPiCamera2:
"""
Configuration for the PiCamera.
"""

def __init__(
self,
timeout: float = 1.0,
exposure_time: int = 250,
analogue_gain: float = 64.0,
contrast: float = 1.0,
maybe_lens_position: float | None = None,
) -> None:
"""
timeout: Getting image timeout in seconds.

exposure_time: Microseconds.
analogue_gain: 0.0 to 64.0 . ISO = Analogue gain * Digital gain * 100 .
contrast: 0.0 to 32.0 . 0.0 is no contrast, 1.0 is normal contrast, higher is more contrast.
lens_position: Position of the lens is dioptres (reciprocal of metres: 1/m ) (0 means infinite distance).
"""
self.timeout = timeout

self.exposure_time = exposure_time
self.analogue_gain = analogue_gain
self.contrast = contrast
self.maybe_lens_position = maybe_lens_position

def to_dict(self) -> dict[str, int | float]:
"""
Dictionary containing camera controls.
"""
camera_controls = {
"ExposureTime": self.exposure_time,
"AnalogueGain": self.analogue_gain,
"Contrast": self.contrast,
}

if self.maybe_lens_position is not None:
camera_controls["LensPosition"] = self.maybe_lens_position
camera_controls["AfMode"] = libcamera.controls.AfModeEnum.Manual
else:
camera_controls["LensPosition"] = 0.0
camera_controls["AfMode"] = libcamera.controls.AfModeEnum.Auto

return camera_controls


class CameraPiCamera2(base_camera.BaseCameraDevice):
Expand All @@ -27,38 +71,51 @@ class CameraPiCamera2(base_camera.BaseCameraDevice):

@classmethod
def create(
cls, width: int, height: int, config: camera_configurations.PiCameraConfig = None
cls, width: int, height: int, config: ConfigPiCamera2
) -> "tuple[True, CameraPiCamera2] | tuple[False, None]":
"""
Picamera2 Camera.

width: Width of the camera.
height: Height of the camera.
config (PiCameraConfig): Configuration object
config: Configuration for PiCamera2 camera.

Return: Success, camera object.
"""
if width <= 0:
return False, None

if height <= 0:
return False, None

try:
camera = picamera2.Picamera2()

camera_config = camera.create_preview_configuration(
{"size": (width, height), "format": "RGB888"}
{"size": (config.image_width, config.image_height), "format": "RGB888"}
)
camera.configure(camera_config)
camera.start()
if config:
controls = config.to_dict()
camera.set_controls(controls)
return True, CameraPiCamera2(cls.__create_key, camera)
controls = config.to_dict()
camera.set_controls(controls)

return True, CameraPiCamera2(cls.__create_key, camera, config)
except RuntimeError:
return False, None

def __init__(self, class_private_create_key: object, camera: "picamera2.Picamera2") -> None:
def __init__(
self,
class_private_create_key: object,
camera: "picamera2.Picamera2",
config: ConfigPiCamera2,
) -> None:
"""
Private constructor, use create() method.
"""
assert class_private_create_key is CameraPiCamera2.__create_key, "Use create() method."

self.__camera = camera
self.__config = config

def __del__(self) -> None:
"""
Expand All @@ -73,8 +130,7 @@ def run(self) -> tuple[True, np.ndarray] | tuple[False, None]:
Return: Success, image with shape (height, width, channels in BGR).
"""
try:
# CAMERA_TIMEOUT seconds before raising TimeoutError
image_data = self.__camera.capture_array(wait=CAMERA_TIMEOUT)
image_data = self.__camera.capture_array(wait=self.__config.timeout)
except TimeoutError:
return False, None

Expand Down
8 changes: 4 additions & 4 deletions tests/integration/test_camera_opencv.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

import cv2

from modules.camera import camera_configurations
from modules.camera import camera_factory
from modules.camera import camera_opencv


# TODO: Add camera logging
Expand All @@ -18,11 +18,11 @@ def main() -> int:
"""
Main function.
"""
config = camera_configurations.OpenCVCameraConfig()
config = camera_opencv.ConfigOpenCV(0)
assert config is not None

result, device = camera_factory.create_camera(
camera_factory.CameraOption.OPENCV, 640, 480, config=config
camera_factory.CameraOption.OPENCV, 640, 480, config
)
if not result:
print("OpenCV camera creation error.")
Expand All @@ -49,7 +49,7 @@ def main() -> int:

if __name__ == "__main__":
result_main = main()
if result_main < 0:
if result_main != 0:
print(f"ERROR: Status code: {result_main}")

print("Done!")
10 changes: 4 additions & 6 deletions tests/integration/test_camera_picamera2.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

import cv2

from modules.camera import camera_configurations
from modules.camera import camera_factory
from modules.camera import camera_picamera2


# TODO: Add camera logging
Expand All @@ -19,13 +19,11 @@ def main() -> int:
Main function.
"""

config = camera_configurations.PiCameraConfig(
exposure_time=250, contrast=1.0, analogue_gain=64.0
)
config = camera_picamera2.ConfigPiCamera2()
assert config.exposure_time == 250
assert config.contrast == 1.0
assert config.analogue_gain == 64.0
assert config.lens_position is None
assert config.maybe_lens_position is None

result, device = camera_factory.create_camera(
camera_factory.CameraOption.PICAM2, 640, 480, config
Expand Down Expand Up @@ -55,7 +53,7 @@ def main() -> int:

if __name__ == "__main__":
result_main = main()
if result_main < 0:
if result_main != 0:
print(f"ERROR: Status code: {result_main}")

print("Done!")
8 changes: 4 additions & 4 deletions tests/integration/test_camera_qr_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@

import cv2

from modules.camera import camera_configurations
from modules.camera import camera_factory
from modules.camera import camera_opencv
from modules.qr import qr_scanner


def main() -> int:
"""
Main function.
"""
config = camera_configurations.OpenCVCameraConfig()
config = camera_opencv.ConfigOpenCV(0)
result, camera = camera_factory.create_camera(
camera_factory.CameraOption.OPENCV, 640, 480, config=config
camera_factory.CameraOption.OPENCV, 640, 480, config
)
if not result:
print("OpenCV camera creation error.")
Expand Down Expand Up @@ -44,7 +44,7 @@ def main() -> int:

if __name__ == "__main__":
result_main = main()
if result_main < 0:
if result_main != 0:
print(f"ERROR: Status code: {result_main}")

print("Done!")
Loading