You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
420 lines
14 KiB
Python
420 lines
14 KiB
Python
"""Android-inspired CameraManager for MicroPythonOS.
|
|
|
|
Provides unified access to camera devices (back-facing, front-facing, external).
|
|
Follows singleton pattern with class method delegation.
|
|
|
|
Example usage:
|
|
from mpos import CameraManager
|
|
|
|
# In board init file:
|
|
CameraManager.add_camera(CameraManager.Camera(
|
|
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK,
|
|
name="OV5640",
|
|
vendor="OmniVision"
|
|
))
|
|
|
|
# In app:
|
|
cam_list = CameraManager.get_cameras()
|
|
if len(cam_list) > 0:
|
|
print("we have a camera!")
|
|
|
|
MIT License
|
|
Copyright (c) 2024 MicroPythonOS contributors
|
|
"""
|
|
|
|
|
|
# Camera lens facing constants (matching Android Camera2 API)
|
|
class CameraCharacteristics:
|
|
"""Camera characteristics and constants."""
|
|
LENS_FACING_BACK = 0 # Back-facing camera (primary)
|
|
LENS_FACING_FRONT = 1 # Front-facing camera (selfie)
|
|
LENS_FACING_EXTERNAL = 2 # External USB camera
|
|
|
|
|
|
class Camera:
|
|
"""Camera metadata (lightweight data class, Android-inspired).
|
|
|
|
Represents a camera device with its characteristics.
|
|
"""
|
|
|
|
def __init__(self, lens_facing, name=None, vendor=None, version=None, init=None, deinit=None, capture=None, apply_settings=None, rotation_degrees=0):
|
|
"""Initialize camera metadata.
|
|
|
|
Args:
|
|
lens_facing: Camera orientation (LENS_FACING_BACK, LENS_FACING_FRONT, etc.)
|
|
name: Human-readable camera name (e.g., "OV5640", "Front Camera")
|
|
vendor: Camera vendor/manufacturer (e.g., "OmniVision")
|
|
version: Driver version (default 1)
|
|
rotation_degrees: how many degrees the camera is rotated clockwise
|
|
"""
|
|
self.lens_facing = lens_facing
|
|
self.name = name or "Camera"
|
|
self.vendor = vendor or "Unknown"
|
|
self.version = version or 1
|
|
self.init_function = init
|
|
self.deinit_function = deinit
|
|
self.capture_function = capture
|
|
self.apply_settings_function = apply_settings
|
|
self.rotation_degrees = rotation_degrees
|
|
|
|
def __repr__(self):
|
|
facing_names = {
|
|
CameraCharacteristics.LENS_FACING_BACK: "BACK",
|
|
CameraCharacteristics.LENS_FACING_FRONT: "FRONT",
|
|
CameraCharacteristics.LENS_FACING_EXTERNAL: "EXTERNAL"
|
|
}
|
|
facing_str = facing_names.get(self.lens_facing, f"UNKNOWN({self.lens_facing})")
|
|
return f"Camera({self.name}, facing={facing_str})"
|
|
|
|
def init(self, width, height, colormode):
|
|
if self.init_function:
|
|
return self.init_function(width, height, colormode)
|
|
|
|
def deinit(self, cam_obj=None):
|
|
if self.deinit_function:
|
|
return self.deinit_function(cam_obj)
|
|
|
|
def capture(self, cam_obj, colormode=None):
|
|
if self.capture_function:
|
|
return self.capture_function(cam_obj, colormode)
|
|
|
|
def apply_settings(self, cam_obj, prefs):
|
|
if self.apply_settings_function:
|
|
return self.apply_settings_function(cam_obj, prefs)
|
|
|
|
def get_rotation_degrees(self):
|
|
return self.rotation_degrees
|
|
|
|
|
|
class CameraManager:
|
|
"""
|
|
Centralized camera device management service.
|
|
Implements singleton pattern for unified camera access.
|
|
|
|
Usage:
|
|
from mpos import CameraManager
|
|
|
|
# Register a camera
|
|
CameraManager.add_camera(CameraManager.Camera(
|
|
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK,
|
|
name="OV5640"
|
|
))
|
|
|
|
# Get all cameras
|
|
cameras = CameraManager.get_cameras()
|
|
"""
|
|
|
|
# Expose inner classes as class attributes
|
|
Camera = Camera
|
|
CameraCharacteristics = CameraCharacteristics
|
|
|
|
_instance = None
|
|
_cameras = [] # Class-level camera list for singleton
|
|
|
|
def __init__(self):
|
|
"""Initialize CameraManager singleton instance."""
|
|
if CameraManager._instance:
|
|
return
|
|
CameraManager._instance = self
|
|
|
|
self._initialized = False
|
|
self.init()
|
|
|
|
@classmethod
|
|
def get(cls):
|
|
"""Get or create the singleton instance."""
|
|
if cls._instance is None:
|
|
cls._instance = cls()
|
|
return cls._instance
|
|
|
|
def init(self):
|
|
"""Initialize CameraManager.
|
|
|
|
Returns:
|
|
bool: True if initialized successfully
|
|
"""
|
|
self._initialized = True
|
|
return True
|
|
|
|
def is_available(self):
|
|
"""Check if CameraManager is initialized.
|
|
|
|
Returns:
|
|
bool: True if CameraManager is initialized
|
|
"""
|
|
return self._initialized
|
|
|
|
def add_camera(self, camera):
|
|
"""Register a camera device.
|
|
|
|
Args:
|
|
camera: Camera object to register
|
|
|
|
Returns:
|
|
bool: True if camera added successfully
|
|
"""
|
|
if not isinstance(camera, Camera):
|
|
print(f"[CameraManager] Error: add_camera() requires Camera object, got {type(camera)}")
|
|
return False
|
|
|
|
# Check if camera with same facing already exists
|
|
for existing in CameraManager._cameras:
|
|
if existing.lens_facing == camera.lens_facing:
|
|
print(f"[CameraManager] Warning: Camera with facing {camera.lens_facing} already registered")
|
|
# Still add it (allow multiple cameras with same facing)
|
|
|
|
CameraManager._cameras.append(camera)
|
|
print(f"[CameraManager] Registered camera: {camera}")
|
|
return True
|
|
|
|
def get_cameras(self):
|
|
"""Get list of all registered cameras.
|
|
|
|
Returns:
|
|
list: List of Camera objects (copy of internal list)
|
|
"""
|
|
return CameraManager._cameras.copy() if CameraManager._cameras else []
|
|
|
|
def get_camera_by_facing(self, lens_facing):
|
|
"""Get first camera with specified lens facing.
|
|
|
|
Args:
|
|
lens_facing: Camera orientation (LENS_FACING_BACK, LENS_FACING_FRONT, etc.)
|
|
|
|
Returns:
|
|
Camera object or None if not found
|
|
"""
|
|
for camera in CameraManager._cameras:
|
|
if camera.lens_facing == lens_facing:
|
|
return camera
|
|
return None
|
|
|
|
def has_camera(self):
|
|
"""Check if any camera is registered.
|
|
|
|
Returns:
|
|
bool: True if at least one camera available
|
|
"""
|
|
return len(CameraManager._cameras) > 0
|
|
|
|
def get_camera_count(self):
|
|
"""Get number of registered cameras.
|
|
|
|
Returns:
|
|
int: Number of cameras
|
|
"""
|
|
return len(CameraManager._cameras)
|
|
|
|
@staticmethod
|
|
def resolution_to_framesize(width, height):
|
|
"""Map resolution (width, height) to FrameSize enum.
|
|
|
|
Args:
|
|
width: Image width in pixels
|
|
height: Image height in pixels
|
|
|
|
Returns:
|
|
FrameSize enum value corresponding to the resolution, or R240X240 as default
|
|
"""
|
|
try:
|
|
from camera import FrameSize
|
|
except ImportError:
|
|
print("Warning: camera module not available")
|
|
return None
|
|
|
|
# Format: (width, height): FrameSize
|
|
resolution_map = {
|
|
(96, 96): FrameSize.R96X96,
|
|
(160, 120): FrameSize.QQVGA,
|
|
(128, 128): FrameSize.R128X128,
|
|
(176, 144): FrameSize.QCIF,
|
|
(240, 176): FrameSize.HQVGA,
|
|
(240, 240): FrameSize.R240X240,
|
|
(320, 240): FrameSize.QVGA,
|
|
(320, 320): FrameSize.R320X320,
|
|
(400, 296): FrameSize.CIF,
|
|
(480, 320): FrameSize.HVGA,
|
|
(480, 480): FrameSize.R480X480,
|
|
(640, 480): FrameSize.VGA,
|
|
(640, 640): FrameSize.R640X640,
|
|
(720, 720): FrameSize.R720X720,
|
|
(800, 600): FrameSize.SVGA,
|
|
(800, 800): FrameSize.R800X800,
|
|
(1024, 768): FrameSize.XGA,
|
|
(960, 960): FrameSize.R960X960,
|
|
(1280, 720): FrameSize.HD,
|
|
(1024, 1024): FrameSize.R1024X1024,
|
|
# These are disabled in camera_settings.py because they use a lot of RAM:
|
|
(1280, 1024): FrameSize.SXGA,
|
|
(1280, 1280): FrameSize.R1280X1280,
|
|
(1600, 1200): FrameSize.UXGA,
|
|
(1920, 1080): FrameSize.FHD,
|
|
}
|
|
|
|
return resolution_map.get((width, height), FrameSize.R240X240)
|
|
|
|
@staticmethod
|
|
def ov_apply_camera_settings(cam, prefs):
|
|
if not cam or not prefs:
|
|
print("ov_apply_camera_settings: Skipping because invalid prefs or cam object")
|
|
return
|
|
|
|
try:
|
|
# Basic image adjustments
|
|
brightness = prefs.get_int("brightness")
|
|
if brightness is not None:
|
|
cam.set_brightness(brightness)
|
|
|
|
contrast = prefs.get_int("contrast")
|
|
if contrast is not None:
|
|
cam.set_contrast(contrast)
|
|
|
|
saturation = prefs.get_int("saturation")
|
|
if saturation is not None:
|
|
cam.set_saturation(saturation)
|
|
|
|
# Orientation
|
|
hmirror = prefs.get_bool("hmirror")
|
|
if hmirror is not None:
|
|
cam.set_hmirror(hmirror)
|
|
|
|
vflip = prefs.get_bool("vflip")
|
|
if vflip is not None:
|
|
cam.set_vflip(vflip)
|
|
|
|
# Special effect
|
|
special_effect = prefs.get_int("special_effect")
|
|
if special_effect is not None:
|
|
cam.set_special_effect(special_effect)
|
|
|
|
# Exposure control (apply master switch first, then manual value)
|
|
exposure_ctrl = prefs.get_bool("exposure_ctrl")
|
|
if exposure_ctrl is not None:
|
|
cam.set_exposure_ctrl(exposure_ctrl)
|
|
else:
|
|
aec_value = prefs.get_int("aec_value")
|
|
if aec_value is not None:
|
|
cam.set_aec_value(aec_value)
|
|
|
|
# Mode-specific default comes from constructor
|
|
ae_level = prefs.get_int("ae_level")
|
|
if ae_level is not None:
|
|
cam.set_ae_level(ae_level)
|
|
|
|
aec2 = prefs.get_bool("aec2")
|
|
if aec2 is not None:
|
|
cam.set_aec2(aec2)
|
|
|
|
# Gain control (apply master switch first, then manual value)
|
|
gain_ctrl = prefs.get_bool("gain_ctrl")
|
|
if gain_ctrl is not None:
|
|
cam.set_gain_ctrl(gain_ctrl)
|
|
else:
|
|
agc_gain = prefs.get_int("agc_gain")
|
|
if agc_gain is None:
|
|
cam.set_agc_gain(agc_gain)
|
|
|
|
gainceiling = prefs.get_int("gainceiling")
|
|
if gainceiling is not None:
|
|
cam.set_gainceiling(gainceiling)
|
|
|
|
# White balance (apply master switch first, then mode)
|
|
whitebal = prefs.get_bool("whitebal")
|
|
if whitebal is not None:
|
|
cam.set_whitebal(whitebal)
|
|
else:
|
|
wb_mode = prefs.get_int("wb_mode")
|
|
if wb_mode is not None:
|
|
cam.set_wb_mode(wb_mode)
|
|
|
|
awb_gain = prefs.get_bool("awb_gain")
|
|
if awb_gain is not None:
|
|
cam.set_awb_gain(awb_gain)
|
|
|
|
# Sensor-specific settings (try/except for unsupported sensors)
|
|
try:
|
|
sharpness = prefs.get_int("sharpness")
|
|
if sharpness is not None:
|
|
cam.set_sharpness(sharpness)
|
|
except:
|
|
pass # Not supported on OV2640?
|
|
|
|
try:
|
|
denoise = prefs.get_int("denoise")
|
|
if denoise is not None:
|
|
cam.set_denoise(denoise)
|
|
except:
|
|
pass # Not supported on OV2640?
|
|
|
|
# Advanced corrections
|
|
colorbar = prefs.get_bool("colorbar")
|
|
if colorbar is not None:
|
|
cam.set_colorbar(colorbar)
|
|
|
|
dcw = prefs.get_bool("dcw")
|
|
if dcw is not None:
|
|
cam.set_dcw(dcw)
|
|
|
|
bpc = prefs.get_bool("bpc")
|
|
if bpc is not None:
|
|
cam.set_bpc(bpc)
|
|
|
|
wpc = prefs.get_bool("wpc")
|
|
if wpc is not None:
|
|
cam.set_wpc(wpc)
|
|
|
|
# Mode-specific default comes from constructor
|
|
raw_gma = prefs.get_bool("raw_gma")
|
|
if raw_gma is not None:
|
|
print(f"applying raw_gma: {raw_gma}")
|
|
cam.set_raw_gma(raw_gma)
|
|
|
|
lenc = prefs.get_bool("lenc")
|
|
if lenc is not None:
|
|
cam.set_lenc(lenc)
|
|
|
|
# JPEG quality (only relevant for JPEG format)
|
|
#try:
|
|
# quality = prefs.get_int("quality", 85)
|
|
# if quality is not None:
|
|
# cam.set_quality(quality)
|
|
#except:
|
|
# pass # Not in JPEG mode
|
|
|
|
print("Camera settings applied successfully")
|
|
|
|
except Exception as e:
|
|
print(f"Error applying camera settings: {e}")
|
|
|
|
|
|
# ============================================================================
|
|
# Class method delegation (at module level)
|
|
# ============================================================================
|
|
|
|
_original_methods = {}
|
|
_methods_to_delegate = [
|
|
'init', 'is_available', 'add_camera', 'get_cameras',
|
|
'get_camera_by_facing', 'has_camera', 'get_camera_count'
|
|
]
|
|
|
|
for method_name in _methods_to_delegate:
|
|
_original_methods[method_name] = getattr(CameraManager, method_name)
|
|
|
|
def _make_class_method(method_name):
|
|
"""Create a class method that delegates to the singleton instance."""
|
|
original_method = _original_methods[method_name]
|
|
|
|
@classmethod
|
|
def class_method(cls, *args, **kwargs):
|
|
instance = cls.get()
|
|
return original_method(instance, *args, **kwargs)
|
|
|
|
return class_method
|
|
|
|
for method_name in _methods_to_delegate:
|
|
setattr(CameraManager, method_name, _make_class_method(method_name))
|
|
|
|
|
|
# Initialize on module load
|
|
CameraManager.init()
|