Skip to content

Commit

Permalink
Move camera configurations close to camera classes (#74)
Browse files Browse the repository at this point in the history
* Move configurations close to camera classes

* Comments
  • Loading branch information
Xierumeng authored Dec 13, 2024
1 parent 1c31b5c commit 9b10a33
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 100 deletions.
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!")

0 comments on commit 9b10a33

Please sign in to comment.