From e2717b2bfb6860bd2e2f568bb55a9dd8b817061e Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Fri, 5 Dec 2025 18:07:13 -0700 Subject: [PATCH 01/11] Refactor drivers to use encapsulation. No more dobule inheritance! New VideoCapture and VideoDisplay classes to more clsoely match standard OpenCV. New "abstract" base classes for camera and display drivers. All camera and display drivers now encapsulate an interface object for better expandability to other platforms. New utils subpackage with common color modes and memory helper functions. Update rv_init package after refactor. Update DVI exampels to no longer use private members. Add header comments files that were previously missing it. Add full file path to header comments. --- red_vision/__init__.py | 8 +- red_vision/cameras/__init__.py | 18 +- red_vision/cameras/cv2_camera.py | 45 ---- red_vision/cameras/dvp_camera.py | 18 +- red_vision/cameras/dvp_rp2_pio.py | 75 ++++--- red_vision/cameras/hm01b0.py | 111 ++++++++-- red_vision/cameras/hm01b0_pio.py | 91 -------- red_vision/cameras/ov5640.py | 115 +++++++--- red_vision/cameras/ov5640_pio.py | 94 --------- red_vision/cameras/video_capture.py | 104 +++++++++ red_vision/cameras/video_capture_driver.py | 36 ++++ red_vision/displays/__init__.py | 21 +- red_vision/displays/dvi.py | 92 ++++++++ red_vision/displays/dvi_rp2_hstx.py | 194 ++++++----------- .../{st7789_spi.py => spi_generic.py} | 26 +-- .../{st7789_pio.py => spi_rp2_pio.py} | 61 ++---- red_vision/displays/st7789.py | 142 +++++++------ .../{cv2_display.py => video_display.py} | 197 ++++++++++-------- red_vision/displays/video_display_driver.py | 21 ++ red_vision/touch_screens/__init__.py | 8 +- red_vision/touch_screens/cst816.py | 20 +- red_vision/touch_screens/cv2_touch_screen.py | 21 -- red_vision/utils/colors.py | 40 ++++ red_vision/utils/memory.py | 66 ++++++ red_vision/utils/pins.py | 61 ++++++ red_vision/utils/video_driver.py | 118 +++++++++++ .../dvi_examples/ex01_hello_dvi.py | 81 +++---- .../dvi_examples/ex02_high_fps_camera.py | 90 +++++--- red_vision_examples/ex01_hello_opencv.py | 2 +- red_vision_examples/ex02_camera.py | 2 +- red_vision_examples/ex03_touch_screen.py | 2 +- red_vision_examples/ex04_imread_imwrite.py | 2 +- red_vision_examples/ex05_performance.py | 2 +- red_vision_examples/ex06_detect_sfe_logo.py | 2 +- red_vision_examples/ex07_animation.py | 2 +- red_vision_examples/rv_init/__init__.py | 38 ++-- red_vision_examples/rv_init/bus_i2c.py | 12 +- red_vision_examples/rv_init/bus_spi.py | 11 +- red_vision_examples/rv_init/camera.py | 106 +++++++--- red_vision_examples/rv_init/display.py | 132 ++++++++---- red_vision_examples/rv_init/sd_card.py | 15 +- red_vision_examples/rv_init/touch_screen.py | 24 ++- .../xrp_examples/ex01_touch_screen_drive.py | 2 +- .../xrp_examples/ex02_grab_orange_ring.py | 2 +- 44 files changed, 1447 insertions(+), 883 deletions(-) delete mode 100644 red_vision/cameras/cv2_camera.py delete mode 100644 red_vision/cameras/hm01b0_pio.py delete mode 100644 red_vision/cameras/ov5640_pio.py create mode 100644 red_vision/cameras/video_capture.py create mode 100644 red_vision/cameras/video_capture_driver.py create mode 100644 red_vision/displays/dvi.py rename red_vision/displays/{st7789_spi.py => spi_generic.py} (84%) rename red_vision/displays/{st7789_pio.py => spi_rp2_pio.py} (74%) rename red_vision/displays/{cv2_display.py => video_display.py} (59%) create mode 100644 red_vision/displays/video_display_driver.py delete mode 100644 red_vision/touch_screens/cv2_touch_screen.py create mode 100644 red_vision/utils/colors.py create mode 100644 red_vision/utils/memory.py create mode 100644 red_vision/utils/pins.py create mode 100644 red_vision/utils/video_driver.py diff --git a/red_vision/__init__.py b/red_vision/__init__.py index 9db91e7..44bee4d 100644 --- a/red_vision/__init__.py +++ b/red_vision/__init__.py @@ -3,11 +3,13 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# cv2_drivers/touch_screens/__init__.py +# red_vision/__init__.py # -# Imports all available drivers for MicroPython OpenCV. +# Imports all available Red Vision drivers, modules, and utilities. #------------------------------------------------------------------------------- -from . import displays from . import cameras +from . import displays from . import touch_screens +from .utils import colors +from .utils import memory diff --git a/red_vision/cameras/__init__.py b/red_vision/cameras/__init__.py index cc46d38..e16896c 100644 --- a/red_vision/cameras/__init__.py +++ b/red_vision/cameras/__init__.py @@ -3,15 +3,19 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# cv2_drivers/cameras/__init__.py +# red_vision/cameras/__init__.py # -# Imports all available camera drivers for MicroPython OpenCV. +# Imports all available Red Vision camera drivers. #------------------------------------------------------------------------------- -# Import sys module to check platform -import sys +# Import the generic VideoCapture class. +from .video_capture import VideoCapture + +# Import platform agnostic drivers. +from .hm01b0 import HM01B0 +from .ov5640 import OV5640 -# Import RP2 drivers +# Import platform specific drivers. +import sys if 'rp2' in sys.platform: - from . import hm01b0_pio - from . import ov5640_pio + from .dvp_rp2_pio import DVP_RP2_PIO diff --git a/red_vision/cameras/cv2_camera.py b/red_vision/cameras/cv2_camera.py deleted file mode 100644 index 4cd362b..0000000 --- a/red_vision/cameras/cv2_camera.py +++ /dev/null @@ -1,45 +0,0 @@ -#------------------------------------------------------------------------------- -# SPDX-License-Identifier: MIT -# -# Copyright (c) 2025 SparkFun Electronics -#------------------------------------------------------------------------------- -# cv2_camera.py -# -# Base class for OpenCV camera drivers. -#------------------------------------------------------------------------------- - -class CV2_Camera(): - """ - Base class for OpenCV camera drivers. - """ - def __init__(self): - """ - Initializes the camera. - """ - pass - - def open(self): - """ - Opens the camera and prepares it for capturing images. - """ - raise NotImplementedError("open() must be implemented by driver") - - def release(self): - """ - Releases the camera and frees any resources. - """ - raise NotImplementedError("release() must be implemented by driver") - - def read(self, image=None): - """ - Reads an image from the camera. - - Args: - image (ndarray, optional): Image to read into - - Returns: - tuple: (success, image) - - success (bool): True if the image was read, otherwise False - - image (ndarray): The captured image, or None if reading failed - """ - raise NotImplementedError("read() must be implemented by driver") diff --git a/red_vision/cameras/dvp_camera.py b/red_vision/cameras/dvp_camera.py index 0799498..3a816a6 100644 --- a/red_vision/cameras/dvp_camera.py +++ b/red_vision/cameras/dvp_camera.py @@ -3,21 +3,25 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# dvp_camera.py +# red_vision/cameras/dvp_camera.py # -# Base class for OpenCV DVP (Digital Video Port) camera drivers. +# Red Vision abstract base class for DVP (Digital Video Port) camera drivers. #------------------------------------------------------------------------------- -from .cv2_camera import CV2_Camera +from .video_capture_driver import VideoCaptureDriver -class DVP_Camera(CV2_Camera): +class DVP_Camera(VideoCaptureDriver): """ - Base class for OpenCV DVP (Digital Video Port) camera drivers. + Red Vision abstract base class for DVP (Digital Video Port) camera drivers. """ def __init__( self, i2c, - i2c_address + i2c_address, + height = None, + width = None, + color_mode = None, + buffer = None, ): """ Initializes the DVP camera with I2C communication. @@ -26,7 +30,7 @@ def __init__( i2c (I2C): I2C object for communication i2c_address (int): I2C address of the camera """ - super().__init__() + super().__init__(height, width, color_mode, buffer) self._i2c = i2c self._i2c_address = i2c_address diff --git a/red_vision/cameras/dvp_rp2_pio.py b/red_vision/cameras/dvp_rp2_pio.py index 530b451..5975ddc 100644 --- a/red_vision/cameras/dvp_rp2_pio.py +++ b/red_vision/cameras/dvp_rp2_pio.py @@ -3,11 +3,10 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# dvp_rp2_pio.py +# red_vision/cameras/dvp_rp2_pio.py # -# This class implements a DVP (Digital Video Port) interface using the RP2 PIO -# (Programmable Input/Output) interface. This is only available on Raspberry Pi -# RP2 processors. +# Red Vision DVP (Digital Video Port) camera interface using the RP2 PIO +# (Programmable Input/Output). Only available on Raspberry Pi RP2 processors. # # This class is derived from: # https://github.com/adafruit/Adafruit_ImageCapture/blob/main/src/arch/rp2040.cpp @@ -19,26 +18,21 @@ import array from machine import Pin, PWM from uctypes import addressof +from ..utils import memory class DVP_RP2_PIO(): """ - This class implements a DVP (Digital Video Port) interface using the RP2 PIO - (Programmable Input/Output) interface. This is only available on Raspberry - Pi RP2 processors. + Red Vision DVP (Digital Video Port) camera interface using the RP2 PIO + (Programmable Input/Output). Only available on Raspberry Pi RP2 processors. """ def __init__( self, + sm_id, pin_d0, pin_vsync, pin_hsync, pin_pclk, - pin_xclk, - xclk_freq, - sm_id, - num_data_pins, - bytes_per_pixel, - byte_swap, - continuous = False + pin_xclk = None, ): """ Initializes the DVP interface with the specified parameters. @@ -62,35 +56,54 @@ def __init__( self._pin_hsync = pin_hsync self._pin_pclk = pin_pclk self._pin_xclk = pin_xclk + self._sm_id = sm_id + + def begin( + self, + buffer, + xclk_freq, + num_data_pins, + byte_swap, + continuous = False, + ): + self._buffer = buffer + self._height, self._width, self._bytes_per_pixel = buffer.shape # Initialize DVP pins as inputs self._num_data_pins = num_data_pins for i in range(num_data_pins): - Pin(pin_d0+i, Pin.IN) - Pin(pin_vsync, Pin.IN) - Pin(pin_hsync, Pin.IN) - Pin(pin_pclk, Pin.IN) + Pin(self._pin_d0+i, Pin.IN) + Pin(self._pin_vsync, Pin.IN) + Pin(self._pin_hsync, Pin.IN) + Pin(self._pin_pclk, Pin.IN) # Set up XCLK pin if provided if self._pin_xclk is not None: - self._xclk = PWM(Pin(pin_xclk)) + self._xclk = PWM(Pin(self._pin_xclk)) self._xclk.freq(xclk_freq) self._xclk.duty_u16(32768) # 50% duty cycle # Store transfer parameters - self._bytes_per_pixel = bytes_per_pixel self._byte_swap = byte_swap # Whether to continuously capture frames self._continuous = continuous # Set up the PIO state machine - self._sm_id = sm_id self._setup_pio() # Set up the DMA controllers self._setup_dmas() + def buffer(self): + """ + Returns the current frame buffer from the camera. + + Returns: + ndarray: Frame buffer + """ + return self._buffer + def _setup_pio(self): # Copy the PIO program program = self._pio_read_dvp @@ -128,22 +141,6 @@ def _pio_read_dvp(): in_(pins, 32) # Mask in number of pins wait(0, gpio, 0) # Mask in PCLK pin - def _is_in_sram(self, data_addr): - """ - Checks whether a given memory address is in SRAM. - - Args: - data_addr (int): Memory address to check - Returns: - bool: True if address is in SRAM, False otherwise - """ - # SRAM address range. - SRAM_BASE = 0x20000000 - total_sram_size = 520*1024 # 520 KB - - # Return whether address is in SRAM. - return data_addr >= SRAM_BASE and data_addr < SRAM_BASE + total_sram_size - def _setup_dmas(self): """ Sets up the DMA controllers for the DVP interface. @@ -239,7 +236,7 @@ def _setup_dmas(self): self._dma_executer = rp2.DMA() # Check if the display buffer is in PSRAM. - self._buffer_is_in_psram = not self._is_in_sram(addressof(self._buffer)) + self._buffer_is_in_psram = memory.is_in_external_ram(self._buffer) # If the buffer is in PSRAM, create the streamer DMA channel and row # buffer in SRAM. @@ -253,7 +250,7 @@ def _setup_dmas(self): # Verify row buffer is in SRAM. If not, we'll still have the same # latency problem. - if not self._is_in_sram(addressof(self._row_buffer)): + if memory.is_in_external_ram(self._row_buffer): raise MemoryError("not enough space in SRAM for row buffer") # Create DMA control register values. diff --git a/red_vision/cameras/hm01b0.py b/red_vision/cameras/hm01b0.py index 83d1bb1..c99a493 100644 --- a/red_vision/cameras/hm01b0.py +++ b/red_vision/cameras/hm01b0.py @@ -3,9 +3,9 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# hm01b0.py +# red_vision/cameras/hm01b0.py # -# Base class for OpenCV HM01B0 camera drivers. +# Red Vision HM01B0 camera driver. # # This class is derived from: # https://github.com/openmv/openmv/blob/5acf5baf92b4314a549bdd068138e5df6cc0bac7/drivers/sensors/hm01b0.c @@ -15,11 +15,11 @@ from .dvp_camera import DVP_Camera from time import sleep_us -import cv2 +from ..utils import colors class HM01B0(DVP_Camera): """ - Base class for OpenCV HM01B0 camera drivers. + Red Vision HM01B0 camera driver. """ # Read only registers _MODEL_ID_H = 0x0000 @@ -234,9 +234,15 @@ class HM01B0(DVP_Camera): def __init__( self, + interface, i2c, i2c_address = 0x24, - num_data_pins = 1 + num_data_pins = 1, + continuous = False, + height = None, + width = None, + color_mode = None, + buffer = None, ): """ Initializes the HM01B0 camera with default settings. @@ -249,11 +255,85 @@ def __init__( - 4 - 8 """ - super().__init__(i2c, i2c_address) + self._interface = interface + self._continuous = continuous + self._num_data_pins = num_data_pins + super().__init__(i2c, i2c_address, height, width, color_mode, buffer) + + self._interface.begin( + self._buffer, + xclk_freq = 25_000_000, + num_data_pins = self._num_data_pins, + byte_swap = False, + continuous = self._continuous, + ) self._soft_reset() - self._send_init(num_data_pins) - + self._send_init(self._num_data_pins) + + def resolution_default(self): + """ + Returns the default resolution of the camera. + + Returns: + tuple: (height, width) in pixels + """ + return (244, 324) + + def resolution_is_supported(self, height, width): + """ + Checks if the specified resolution is supported by the camera. + + Args: + height (int): Image height in pixels + width (int): Image width in pixels + Returns: + bool: True if the resolution is supported, otherwise False + """ + return (height, width) == (244, 324) + + def color_mode_default(self): + """ + Returns the default color mode of the camera. + + Returns: + int: Color mode constant + """ + return colors.COLOR_MODE_BAYER_RG + + def color_mode_is_supported(self, color_mode): + """ + Checks if the specified color mode is supported by the camera. + + Args: + color_mode (int): Color mode constant + Returns: + bool: True if the color mode is supported, otherwise False + """ + return color_mode == colors.COLOR_MODE_BAYER_RG + + def open(self): + """ + Opens the camera and prepares it for capturing images. + """ + pass + + def release(self): + """ + Releases the camera and frees any resources. + """ + pass + + def grab(self): + """ + Grabs a single frame from the camera. + + Returns: + bool: True if the frame was grabbed successfully, otherwise False + """ + self._interface._capture() + return True + def _is_connected(self): """ Checks if the camera is connected by reading the chip ID. @@ -336,18 +416,3 @@ def _send_init(self, num_data_pins): value = 0x02 self._write_register(reg, value) sleep_us(1000) - - def read(self, image=None): - """ - Reads an image from the camera. - - Args: - image (ndarray, optional): Image to read into - - Returns: - tuple: (success, image) - - success (bool): True if the image was read, otherwise False - - image (ndarray): The captured image, or None if reading failed - """ - self._capture() - return (True, cv2.cvtColor(self._buffer, cv2.COLOR_BayerRG2BGR, image)) diff --git a/red_vision/cameras/hm01b0_pio.py b/red_vision/cameras/hm01b0_pio.py deleted file mode 100644 index 8135dc2..0000000 --- a/red_vision/cameras/hm01b0_pio.py +++ /dev/null @@ -1,91 +0,0 @@ -#------------------------------------------------------------------------------- -# SPDX-License-Identifier: MIT -# -# Copyright (c) 2025 SparkFun Electronics -#------------------------------------------------------------------------------- -# hm01b0_pio.py -# -# OpenCV HM01B0 camera driver using a PIO interface. Only available on -# Raspberry Pi RP2 processors. -#------------------------------------------------------------------------------- - -from .hm01b0 import HM01B0 -from .dvp_rp2_pio import DVP_RP2_PIO -from ulab import numpy as np - -class HM01B0_PIO(HM01B0, DVP_RP2_PIO): - """ - OpenCV HM01B0 camera driver using a PIO interface. Only available on - Raspberry Pi RP2 processors. - """ - def __init__( - self, - i2c, - sm_id, - pin_d0, - pin_vsync, - pin_hsync, - pin_pclk, - pin_xclk = None, - xclk_freq = 25_000_000, - num_data_pins = 1, - i2c_address = 0x24, - continuous = False, - ): - """ - Initializes the HM01B0 PIO camera driver. - - Args: - i2c (I2C): I2C object for communication - sm_id (int): PIO state machine ID - pin_d0 (int): Data 0 pin number for DVP interface - pin_vsync (int): Vertical sync pin number - pin_hsync (int): Horizontal sync pin number - pin_pclk (int): Pixel clock pin number - pin_xclk (int, optional): External clock pin number - xclk_freq (int, optional): Frequency in Hz for the external clock - Default is 25 MHz - num_data_pins (int, optional): Number of data pins used in DVP interface - Default is 1 - i2c_address (int, optional): I2C address of the camera - Default is 0x24 - """ - # Create the frame buffer - self._width = 324 - self._height = 244 - self._bytes_per_pixel = 1 - self._buffer = np.zeros((244, 324), dtype=np.uint8) - - # Call both parent constructors - DVP_RP2_PIO.__init__( - self, - pin_d0, - pin_vsync, - pin_hsync, - pin_pclk, - pin_xclk, - xclk_freq, - sm_id, - num_data_pins, - bytes_per_pixel = 2, - byte_swap = True, - continuous = continuous, - ) - HM01B0.__init__( - self, - i2c, - i2c_address, - num_data_pins - ) - - def open(self): - """ - Opens the camera and prepares it for capturing images. - """ - pass - - def release(self): - """ - Releases the camera and frees any resources. - """ - pass diff --git a/red_vision/cameras/ov5640.py b/red_vision/cameras/ov5640.py index ecad3b8..f0c79a4 100644 --- a/red_vision/cameras/ov5640.py +++ b/red_vision/cameras/ov5640.py @@ -3,9 +3,9 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# ov5640.py +# red_vision/cameras/ov5640.py # -# Base class for OpenCV OV5640 camera drivers. +# Red Vision OV5640 camera driver. # # This class is derived from: # https://github.com/adafruit/Adafruit_CircuitPython_OV5640 @@ -15,11 +15,11 @@ from .dvp_camera import DVP_Camera from time import sleep_us -import cv2 +from ..utils import colors class OV5640(DVP_Camera): """ - Base class for OpenCV OV5640 camera drivers. + Red Vision OV5640 camera driver. """ _OV5640_COLOR_RGB = 0 _OV5640_COLOR_YUV = 1 @@ -888,8 +888,14 @@ class OV5640(DVP_Camera): def __init__( self, + interface, i2c, - i2c_address = 0x3C + i2c_address = 0x3C, + continuous = False, + height = None, + width = None, + color_mode = None, + buffer = None, ): """ Initializes the OV5640 camera sensor with default settings. @@ -898,7 +904,17 @@ def __init__( i2c (I2C): I2C object for communication i2c_address (int, optional): I2C address (default: 0x3C) """ - super().__init__(i2c, i2c_address) + self._interface = interface + self._continuous = continuous + super().__init__(i2c, i2c_address, height, width, color_mode, buffer) + + self._interface.begin( + self._buffer, + xclk_freq = 20_000_000, + num_data_pins = 8, + byte_swap = False, + continuous = self._continuous, + ) self._write_list(self._sensor_default_regs) @@ -915,7 +931,70 @@ def __init__( self._white_balance = 0 self._set_size_and_colorspace() - + + def resolution_default(self): + """ + Returns the default resolution of the camera. + + Returns: + tuple: (height, width) in pixels + """ + return (240, 320) + + def resolution_is_supported(self, height, width): + """ + Checks if the specified resolution is supported by the camera. + + Args: + height (int): Image height in pixels + width (int): Image width in pixels + Returns: + bool: True if the resolution is supported, otherwise False + """ + return (height, width) == (240, 320) + + def color_mode_default(self): + """ + Returns the default color mode of the camera. + + Returns: + int: Color mode constant + """ + return colors.COLOR_MODE_BGR565 + + def color_mode_is_supported(self, color_mode): + """ + Checks if the specified color mode is supported by the camera. + + Args: + color_mode (int): Color mode constant + Returns: + bool: True if the color mode is supported, otherwise False + """ + return color_mode == colors.COLOR_MODE_BGR565 + + def open(self): + """ + Opens the camera and prepares it for capturing images. + """ + pass + + def release(self): + """ + Releases the camera and frees any resources. + """ + pass + + def grab(self): + """ + Grabs a single frame from the camera. + + Returns: + bool: True if the frame was grabbed successfully, otherwise False + """ + self._interface._capture() + return True + def _is_connected(self): """ Checks if the camera is connected by reading the chip ID. @@ -1167,25 +1246,3 @@ def _write_reg_bits(self, reg: int, mask: int, enable: bool) -> None: else: val &= ~mask self._write_register(reg, val) - - def read(self, image = None): - """ - Reads an image from the camera. - - Args: - image (ndarray, optional): Image to read into - - Returns: - tuple: (success, image) - - success (bool): True if the image was read, otherwise False - - image (ndarray): The captured image, or None if reading failed - """ - self._capture() - if self._colorspace == self._OV5640_COLOR_RGB: - return (True, cv2.cvtColor(self._buffer, cv2.COLOR_BGR5652BGR, image)) - elif self._colorspace == self._OV5640_COLOR_GRAYSCALE: - return (True, cv2.cvtColor(self._buffer, cv2.COLOR_GRAY2BGR, image)) - else: - NotImplementedError( - f"OV5640: Reading images in colorspace {self._colorspace} is not yet implemented." - ) diff --git a/red_vision/cameras/ov5640_pio.py b/red_vision/cameras/ov5640_pio.py deleted file mode 100644 index 9b20ac8..0000000 --- a/red_vision/cameras/ov5640_pio.py +++ /dev/null @@ -1,94 +0,0 @@ -#------------------------------------------------------------------------------- -# SPDX-License-Identifier: MIT -# -# Copyright (c) 2025 SparkFun Electronics -#------------------------------------------------------------------------------- -# ov5640_pio.py -# -# OpenCV OV5640 camera driver using a PIO interface. Only available on -# Raspberry Pi RP2 processors. -#------------------------------------------------------------------------------- - -from .ov5640 import OV5640 -from .dvp_rp2_pio import DVP_RP2_PIO -from ulab import numpy as np - -class OV5640_PIO(OV5640, DVP_RP2_PIO): - """ - OpenCV OV5640 camera driver using a PIO interface. Only available on - Raspberry Pi RP2 processors. - """ - def __init__( - self, - i2c, - sm_id, - pin_d0, - pin_vsync, - pin_hsync, - pin_pclk, - pin_xclk = None, - xclk_freq = 20_000_000, - i2c_address = 0x3c, - buffer = None, - continuous = False, - ): - """ - Initializes the OV5640 PIO camera driver. - - Args: - i2c (I2C): I2C object for communication - sm_id (int): PIO state machine ID - pin_d0 (int): Data 0 pin number for DVP interface - pin_vsync (int): Vertical sync pin number - pin_hsync (int): Horizontal sync pin number - pin_pclk (int): Pixel clock pin number - pin_xclk (int, optional): External clock pin number - xclk_freq (int, optional): Frequency in Hz for the external clock - Default is 5 MHz - i2c_address (int, optional): I2C address of the camera - Default is 0x3c - buffer (ndarray, optional): Pre-allocated frame buffer. - continuous (bool, optional): Whether to run in continuous mode. - """ - # Create the frame buffer - if buffer is not None: - self._buffer = buffer - self._height, self._width, self._bytes_per_pixel = buffer.shape - else: - self._width = 320 - self._height = 240 - self._bytes_per_pixel = 2 - self._buffer = np.zeros((240, 320, 2), dtype=np.uint8) - - # Call both parent constructors - DVP_RP2_PIO.__init__( - self, - pin_d0, - pin_vsync, - pin_hsync, - pin_pclk, - pin_xclk, - xclk_freq, - sm_id, - num_data_pins = 8, - bytes_per_pixel = 2, - byte_swap = False, - continuous = continuous, - ) - OV5640.__init__( - self, - i2c, - i2c_address - ) - - def open(self): - """ - Opens the camera and prepares it for capturing images. - """ - pass - - def release(self): - """ - Releases the camera and frees any resources. - """ - pass diff --git a/red_vision/cameras/video_capture.py b/red_vision/cameras/video_capture.py new file mode 100644 index 0000000..75653d5 --- /dev/null +++ b/red_vision/cameras/video_capture.py @@ -0,0 +1,104 @@ +#------------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 SparkFun Electronics +#------------------------------------------------------------------------------- +# red_vision/cameras/video_capture.py +# +# Red Vision generic camera class. This is implemented like standard OpenCV's +# VideoCapture class. +#------------------------------------------------------------------------------- + +import cv2 +from ..utils import colors + +class VideoCapture(): + """ + Red Vision generic camera class. This is implemented like standard OpenCV's + VideoCapture class. + """ + def __init__( + self, + driver, + ): + """ + Initializes the camera. + """ + # Store driver reference. + self._driver = driver + + def open(self): + """ + Opens the camera and prepares it for capturing images. + """ + self._driver.open() + + def release(self): + """ + Releases the camera and frees any resources. + """ + self._driver.release() + + def grab(self): + """ + Grabs a single frame from the camera. + + Returns: + bool: True if the frame was grabbed successfully, otherwise False + """ + return self._driver.grab() + + def retrieve(self, image = None): + """ + Retrieves the most recently grabbed frame from the camera. + + Args: + image (ndarray, optional): Image to retrieve into + Returns: + tuple: (success, image) + - success (bool): True if the image was retrieved, otherwise False + - image (ndarray): The retrieved image, or None if retrieval failed + """ + color_mode = self._driver.color_mode() + buffer = self._driver.buffer() + if (color_mode == colors.COLOR_MODE_BGR888 or + color_mode == colors.COLOR_MODE_GRAY8 or + color_mode == colors.COLOR_MODE_BGR233): # No conversion available + # These color modes are copied directly with no conversion. + if image is not None: + # Copy buffer to provided image. + image[:] = buffer + return (True, image) + else: + # Return a copy of the buffer. + return (True, buffer.copy()) + elif color_mode == colors.COLOR_MODE_BAYER_BG: + return (True, cv2.cvtColor(buffer, cv2.COLOR_BayerBG2BGR, image)) + elif color_mode == colors.COLOR_MODE_BAYER_GB: + return (True, cv2.cvtColor(buffer, cv2.COLOR_BayerGB2BGR, image)) + elif color_mode == colors.COLOR_MODE_BAYER_RG: + return (True, cv2.cvtColor(buffer, cv2.COLOR_BayerRG2BGR, image)) + elif color_mode == colors.COLOR_MODE_BAYER_GR: + return (True, cv2.cvtColor(buffer, cv2.COLOR_BayerGR2BGR, image)) + elif color_mode == colors.COLOR_MODE_BGR565: + return (True, cv2.cvtColor(buffer, cv2.COLOR_BGR5652BGR, image)) + elif color_mode == colors.COLOR_MODE_BGRA8888: + return (True, cv2.cvtColor(buffer, cv2.COLOR_BGRA2BGR, image)) + else: + NotImplementedError("Unsupported color mode") + + def read(self, image = None): + """ + Reads an image from the camera. + + Args: + image (ndarray, optional): Image to read into + + Returns: + tuple: (success, image) + - success (bool): True if the image was read, otherwise False + - image (ndarray): The captured image, or None if reading failed + """ + if not self.grab(): + return (False, None) + return self.retrieve(image = image) diff --git a/red_vision/cameras/video_capture_driver.py b/red_vision/cameras/video_capture_driver.py new file mode 100644 index 0000000..850b1ad --- /dev/null +++ b/red_vision/cameras/video_capture_driver.py @@ -0,0 +1,36 @@ +#------------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 SparkFun Electronics +#------------------------------------------------------------------------------- +# red_vision/utils/video_capture_driver.py +# +# Red Vision abstract base class for camera drivers. +#------------------------------------------------------------------------------- + +from ..utils.video_driver import VideoDriver + +class VideoCaptureDriver(VideoDriver): + """ + Red Vision abstract base class for camera drivers. + """ + def open(self): + """ + Opens the camera and prepares it for capturing images. + """ + raise NotImplementedError("Subclass must implement this method") + + def release(self): + """ + Releases the camera and frees any resources. + """ + raise NotImplementedError("Subclass must implement this method") + + def grab(self): + """ + Grabs a single frame from the camera. + + Returns: + bool: True if the frame was grabbed successfully, otherwise False + """ + raise NotImplementedError("Subclass must implement this method") diff --git a/red_vision/displays/__init__.py b/red_vision/displays/__init__.py index be5988a..6cff374 100644 --- a/red_vision/displays/__init__.py +++ b/red_vision/displays/__init__.py @@ -3,18 +3,21 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# cv2_drivers/displays/__init__.py +# red_vision/displays/__init__.py # -# Imports all available display drivers for MicroPython OpenCV. +# Imports all available Red Vision display drivers. #------------------------------------------------------------------------------- -# Import platform agnostic drivers -from . import st7789_spi +# Import the generic VideoDisplay class. +from .video_display import VideoDisplay -# Import sys module to check platform -import sys +# Import platform agnostic drivers. +from .st7789 import ST7789 +from .spi_generic import SPI_Generic +from .dvi import DVI -# Import RP2 drivers +# Import platform specific drivers. +import sys if 'rp2' in sys.platform: - from . import st7789_pio - from . import dvi_rp2_hstx + from .spi_rp2_pio import SPI_RP2_PIO + from .dvi_rp2_hstx import DVI_RP2_HSTX diff --git a/red_vision/displays/dvi.py b/red_vision/displays/dvi.py new file mode 100644 index 0000000..5c7e4e2 --- /dev/null +++ b/red_vision/displays/dvi.py @@ -0,0 +1,92 @@ +#------------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 SparkFun Electronics +#------------------------------------------------------------------------------- +# red_vision/displays/dvi.py +# +# Red Vision DVI display driver. +#------------------------------------------------------------------------------- + +from .video_display_driver import VideoDisplayDriver + +class DVI(VideoDisplayDriver): + """ + Red Vision DVI display driver. + """ + def __init__( + self, + interface, + height = None, + width = None, + color_mode = None, + buffer = None, + ): + """ + Initializes the DVI display driver. + + Args: + width (int): Display width in pixels + height (int): Display height in pixels + rotation (int, optional): Orientation of display + - 0: Portrait (default) + - 1: Landscape + - 2: Inverted portrait + - 3: Inverted landscape + bgr_order (bool, optional): Color order + - True: BGR (default) + - False: RGB + reverse_bytes_in_word (bool, optional): + - Enable if the display uses LSB byte order for color words + """ + self._interface = interface + super().__init__(height, width, color_mode, buffer) + + self._interface.begin( + self._buffer, + self._color_mode, + ) + + def resolution_default(self): + """ + Returns the default resolution for the display. + + Returns: + tuple: (height, width) in pixels + """ + return self._interface.resolution_default() + + def resolution_is_supported(self, height, width): + """ + Checks if the given resolution is supported by the display. + + Args: + height (int): Height in pixels + width (int): Width in pixels + Returns: + bool: True if the resolution is supported, otherwise False + """ + return self._interface.resolution_is_supported(height, width) + + def color_mode_default(self): + """ + Returns the default color mode for the display. + """ + return self._interface.color_mode_default() + + def color_mode_is_supported(self, color_mode): + """ + Checks if the given color mode is supported by the display. + + Args: + color_mode (int): Color mode to check + Returns: + bool: True if the color mode is supported, otherwise False + """ + return self._interface.color_mode_is_supported(color_mode) + + def show(self): + """ + Updates the display with the contents of the framebuffer. + """ + pass diff --git a/red_vision/displays/dvi_rp2_hstx.py b/red_vision/displays/dvi_rp2_hstx.py index 7bf669d..7f3cd5c 100644 --- a/red_vision/displays/dvi_rp2_hstx.py +++ b/red_vision/displays/dvi_rp2_hstx.py @@ -1,12 +1,12 @@ - #------------------------------------------------------------------------------- # SPDX-License-Identifier: MIT # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# dvp_rp2_hstx.py -# -# OpenCV DVI display driver using the RP2350 HSTX interface. +# red_vision/displays/dvi_rp2_hstx.py +# +# Red Vision DVI/HDMI display driver using the RP2350 HSTX interface. Only +# available on Raspberry Pi RP2 processors. # # This class is partially derived from: # https://github.com/adafruit/circuitpython/blob/main/ports/raspberrypi/common-hal/picodvi/Framebuffer_RP2350.c @@ -15,28 +15,22 @@ #------------------------------------------------------------------------------- # Imports -from .cv2_display import CV2_Display import rp2 import machine import array from uctypes import addressof -import cv2 as cv from ulab import numpy as np +from ..utils import colors +from ..utils import memory -class DVI_HSTX(CV2_Display): +class DVI_RP2_HSTX(): """ - OpenCV DVI display driver using an HSTX interface. Only available on - Raspberry Pi RP2350. + Red Vision DVI/HDMI display driver using the RP2350 HSTX interface. Only + available on Raspberry Pi RP2 processors. Because the HSTX is capable of double data rate (DDR) signaling, it is the fastest way to output DVI from the RP2350. """ - # Supported color modes. - COLOR_BGR233 = 0 - COLOR_GRAY8 = 1 - COLOR_BGR565 = 2 - COLOR_BGRA8888 = 3 - # Below is a reference video timing diagram. Source: # https://projectf.io/posts/video-timings-vga-720p-1080p/#video-signals-in-brief # @@ -113,9 +107,6 @@ class DVI_HSTX(CV2_Display): def __init__( self, - width, - height, - color_mode = COLOR_BGR565, pin_clk_p = 14, pin_clk_n = 15, pin_d0_p = 18, @@ -124,7 +115,6 @@ def __init__( pin_d1_n = 17, pin_d2_p = 12, pin_d2_n = 13, - buffer = None, ): """ Initializes the DVI HSTX display driver. @@ -157,29 +147,18 @@ def __init__( self._pin_d2_p = pin_d2_p self._pin_d2_n = pin_d2_n - # Set color mode and bytes per pixel. + def begin(self, buffer, color_mode): + """ + Begins DVI output. + """ + # Store buffer and color mode. + self._buffer = buffer self._color_mode = color_mode - if color_mode == self.COLOR_BGR233 or color_mode == self.COLOR_GRAY8: - self._bytes_per_pixel = 1 - elif color_mode == self.COLOR_BGR565: - self._bytes_per_pixel = 2 - elif color_mode == self.COLOR_BGRA8888: - self._bytes_per_pixel = 4 + self._height, self._width, self._bytes_per_pixel = self._buffer.shape # Set resolution and scaling factors. - self._width = width - self._height = height - self._width_scale = self._H_ACTIVE_PIXELS // width - self._height_scale = self._V_ACTIVE_LINES // height - - # Create the image buffer. - if buffer is not None: - self._buffer = buffer - else: - self._buffer = np.zeros( - (height, width, self._bytes_per_pixel), - dtype = np.uint8 - ) + self._width_scale = self._H_ACTIVE_PIXELS // self._width + self._height_scale = self._V_ACTIVE_LINES // self._height # Configure HSTX peripheral. self._configure_hstx() @@ -190,98 +169,49 @@ def __init__( # Start DVI output. self._start() - def imshow(self, image): + def resolution_default(self): """ - Shows a NumPy image on the display. - - Args: - image (ndarray): Image to show + Returns the default resolution for the display. """ - # Get the common ROI between the image and internal display buffer - image_roi, buffer_roi = self._get_common_roi_with_buffer(image) - - # Ensure the image is in uint8 format - image_roi = self._convert_to_uint8(image_roi) - - # Convert the image to current format and write it to the buffer - if self._color_mode == self.COLOR_GRAY8 or self._color_mode == self.COLOR_BGR233: - # OpenCV doesn't have a BGR233 conversion, so use grayscale since - # it's also 8-bit. If the input image is BGR233, the output will - # be unchanged. - self._convert_to_gray8(image_roi, buffer_roi) - elif self._color_mode == self.COLOR_BGR565: - self._convert_to_bgr565(image_roi, buffer_roi) - elif self._color_mode == self.COLOR_BGRA8888: - self._convert_to_bgra8888(image_roi, buffer_roi) - else: - raise ValueError("Unsupported color mode") - - def clear(self): - """ - Clears the display by filling it with black color. - """ - # Clear the buffer by filling it with zeros (black) - self._buffer[:] = 0 + return (self._V_ACTIVE_LINES // 2, self._H_ACTIVE_PIXELS // 2) - def _is_in_sram(self, data_addr): + def resolution_is_supported(self, height, width): """ - Checks whether a given memory address is in SRAM. + Checks if the given resolution is supported by the display. Args: - data_addr (int): Memory address to check + height (int): Height in pixels + width (int): Width in pixels Returns: - bool: True if address is in SRAM, False otherwise + bool: True if the resolution is supported, otherwise False """ - # SRAM address range. - SRAM_BASE = 0x20000000 - SRAM_END = 0x20082000 + # Check if width and height are factors of active pixels/lines. + width_supported = (self._H_ACTIVE_PIXELS % width == 0) + height_supported = (self._V_ACTIVE_LINES % height == 0) + + # Check if either is not a factor. + if not width_supported or not height_supported: + return False - # Return whether address is in SRAM. - return data_addr >= SRAM_BASE and data_addr < SRAM_END + # Both are factors, but width can only be upscaled to a maximum of 32x. + return self._H_ACTIVE_PIXELS / width <= 32 - def _check_psram_transfer_speed(self): + def color_mode_default(self): + """ + Returns the default color mode for the display. """ - Checks whether the PSRAM transfer speed is sufficient for specified - resolution and color mode. + return colors.COLOR_MODE_BGR565 + def color_mode_is_supported(self, color_mode): + """ + Checks if the given color mode is supported by the display. + + Args: + color_mode (int): Color mode to check Returns: - bool: True if PSRAM speed is sufficient, False otherwise. + bool: True if the color mode is supported, otherwise False """ - # The RP2350 system clock is typically 150 MHz, and the HSTX transmits 1 - # pixel every 5 clock cycles, so 30 megapixels per second. The QSPI bus - # clock is typically half the system clock (150 MHz / 2 = 75 MHz), and 1 - # byte per 2 clock cycles (quad-SPI), so 37.5 Mbytes/second. So for - # native resolution (no scaling), only color modes with 1 byte per pixel - # are possible (eg. BGR233 or GRAY8). Larger color modes (2 or 4 bytes - # per pixel) can only be used with scaling. - - # PSRAM timing register parameters. - XIP_QMI_BASE = 0x400D0000 - M1_TIMING = XIP_QMI_BASE + 0x20 - CLKDIV_MASK = 0xFF - - # Get PSRAM clock divider, typically 2. - psram_clk_div = machine.mem32[M1_TIMING] & CLKDIV_MASK - - # Compute PSRAM pixel transfer rate. PSRAM is on the QSPI bus, which - # transfers 1 byte every 2 clock cycles. - psram_clock_hz = machine.freq() / psram_clk_div # Typically 75 MHz - psram_bytes_per_second = psram_clock_hz / 2 # Typically 37.5 MBps - psram_pixels_per_second = psram_bytes_per_second * self._width_scale / self._bytes_per_pixel - - # The HSTX configuration sends 1 pixel every 5 system clock cycles, - # ignoring sync/porch timing signals. - hstx_pixels_per_second = machine.freq() / 5 - - # Probing with an oscilloscope has shown that the XIP stream typically - # performs transfers in 32 bit bursts every 19 system clock cycles - # (~127ns) instead of the nominal 16 system clock cycles (~107ns). This - # could be relevant if the PSRAM and HSTX speeds are close, so we'll - # include it as a safety margin. - psram_pixels_per_second *= 16 / 19 - - # Return whether PSRAM transfer speed is sufficient. - return psram_pixels_per_second > hstx_pixels_per_second + return color_mode == colors.COLOR_MODE_BGR565 def _configure_hstx(self): """ @@ -306,7 +236,7 @@ def _configure_hstx(self): # FIFO is one complete timing symbol, so `raw_n_shifts` and `raw_shift` # are set to 1 and 0, respectively. expand_shift = self._hstx.pack_expand_shift( - enc_n_shifts = self._width_scale, + enc_n_shifts = self._width_scale % 32, enc_shift = 0, raw_n_shifts = 1, raw_shift = 0 @@ -331,7 +261,7 @@ def _configure_hstx(self): # With BGR color modes, B is the least significant bits, and R is the # most significant bits. This means the bits are in RGB order, which is # opposite of what one might expect. - if self._color_mode == self.COLOR_BGR233: + if self._color_mode == colors.COLOR_MODE_BGR233: # BGR233 (00000000 00000000 00000000 RRRGGGBB) expand_tmds = self._hstx.pack_expand_tmds( l2_nbits = 2, # 3 bits (red) @@ -341,7 +271,7 @@ def _configure_hstx(self): l0_nbits = 1, # 2 bits (blue) l0_rot = 26, # Shift right 26 bits to align MSB (left 6 bits) ) - elif self._color_mode == self.COLOR_GRAY8: + elif self._color_mode == colors.COLOR_MODE_GRAY8: # GRAY8 (00000000 00000000 00000000 GGGGGGGG) expand_tmds = self._hstx.pack_expand_tmds( l2_nbits = 7, # 8 bits (red) @@ -351,7 +281,7 @@ def _configure_hstx(self): l0_nbits = 7, # 8 bits (blue) l0_rot = 0, # Shift right 0 bits to align MSB ) - elif self._color_mode == self.COLOR_BGR565: + elif self._color_mode == colors.COLOR_MODE_BGR565: # BGR565 (00000000 00000000 RRRRRGGG GGGBBBBB) expand_tmds = self._hstx.pack_expand_tmds( l2_nbits = 4, # 5 bits (red) @@ -361,7 +291,7 @@ def _configure_hstx(self): l0_nbits = 4, # 5 bits (blue) l0_rot = 29, # Shift right 29 bits to align MSB (left 3 bits) ) - elif self._color_mode == self.COLOR_BGRA8888: + elif self._color_mode == colors.COLOR_MODE_BGRA8888: # BGRA8888 (AAAAAAAA RRRRRRRR GGGGGGGG BBBBBBBB) alpha is ignored expand_tmds = self._hstx.pack_expand_tmds( l2_nbits = 7, # 8 bits (red) @@ -596,7 +526,7 @@ def _configure_dmas(self): self._dma_executer = rp2.DMA() # Check if the display buffer is in PSRAM. - self._buffer_is_in_psram = not self._is_in_sram(addressof(self._buffer)) + self._buffer_is_in_psram = memory.is_in_external_ram(self._buffer) # If the buffer is in PSRAM, create the streamer DMA channel and row # buffer in SRAM. @@ -605,8 +535,18 @@ def _configure_dmas(self): self._dma_streamer = rp2.DMA() # Verify that PSRAM transfer speed is sufficient for specified - # resolution and color mode. - if not self._check_psram_transfer_speed(): + # resolution and color mode. The RP2350 system clock is typically + # 150 MHz, and the HSTX transmits 1 pixel every 5 clock cycles, so + # 30 megapixels per second. The QSPI bus clock is typically half the + # system clock (150 MHz / 2 = 75 MHz), and 1 byte per 2 clock cycles + # (quad-SPI), so 37.5 Mbytes/second. So for native resolution (no + # scaling), only color modes with 1 byte per pixel are possible (eg. + # BGR233 or GRAY8). Larger color modes (2 or 4 bytes per pixel) can + # only be used with scaling. + hstx_pixels_per_second = machine.freq() / 5 + psram_bytes_per_second = memory.external_ram_max_bytes_per_second() + psram_pixels_per_second = psram_bytes_per_second * self._width_scale / self._bytes_per_pixel + if psram_pixels_per_second < hstx_pixels_per_second: raise ValueError("PSRAM transfer speed too low for specified resolution and color mode") # Create the row buffer. @@ -615,7 +555,7 @@ def _configure_dmas(self): # Verify row buffer is in SRAM. If not, we'll still have the same # latency problem. - if not self._is_in_sram(addressof(self._row_buffer)): + if memory.is_in_external_ram(self._row_buffer): raise MemoryError("not enough space in SRAM for row buffer") # We'll use a DMA to trigger the XIP stream. However the RP2350's @@ -785,7 +725,7 @@ def _create_control_blocks(self): # The control block array must be in SRAM, otherwise we run into the # same latency problem with DMA transfers from PSRAM. - if not self._is_in_sram(addressof(self._control_blocks)): + if memory.is_in_external_ram(self._control_blocks): raise MemoryError("not enough space in SRAM for control block array") # Create the HSTX command sequences so the control blocks can reference diff --git a/red_vision/displays/st7789_spi.py b/red_vision/displays/spi_generic.py similarity index 84% rename from red_vision/displays/st7789_spi.py rename to red_vision/displays/spi_generic.py index 13c34f9..212ff03 100644 --- a/red_vision/displays/st7789_spi.py +++ b/red_vision/displays/spi_generic.py @@ -3,9 +3,9 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# st7789_spi.py +# red_vision/displays/spi_generic.py # -# OpenCV ST7789 display driver using a SPI interface. +# Red Vision SPI display driver using a generic SPI interface. # # This class is derived from: # https://github.com/easytarget/st7789-framebuffer/blob/main/st7789_purefb.py @@ -16,23 +16,18 @@ # Copyright (c) 2019 Ivan Belokobylskiy #------------------------------------------------------------------------------- -from .st7789 import ST7789 from machine import Pin +from ..utils.pins import save_pin_mode_alt -class ST7789_SPI(ST7789): +class SPI_Generic(): """ - OpenCV ST7789 display driver using a SPI interface. + Red Vision SPI display driver using a generic SPI interface. """ def __init__( self, - width, - height, spi, pin_dc, pin_cs=None, - rotation=0, - bgr_order=True, - reverse_bytes_in_word=True, ): """ Initializes the ST7789 SPI display driver. @@ -59,9 +54,14 @@ def __init__( self._dc = Pin(pin_dc) # Don't change mode/alt self._cs = Pin(pin_cs, Pin.OUT, value=1) if pin_cs else None - super().__init__(width, height, rotation, bgr_order, reverse_bytes_in_word) + def begin(self): + """ + Initializes the SPI interface for the display. + """ + # Nothing to do for SPI + pass - def _write(self, command=None, data=None): + def write(self, command=None, data=None): """ Writes commands and data to the display. @@ -71,7 +71,7 @@ def _write(self, command=None, data=None): """ # Save the current mode and alt of the DC pin in case it's used by # another device on the same SPI bus - dcMode, dcAlt = self._save_pin_mode_alt(self._dc) + dcMode, dcAlt = save_pin_mode_alt(self._dc) # Temporarily set the DC pin to output mode self._dc.init(mode=Pin.OUT) diff --git a/red_vision/displays/st7789_pio.py b/red_vision/displays/spi_rp2_pio.py similarity index 74% rename from red_vision/displays/st7789_pio.py rename to red_vision/displays/spi_rp2_pio.py index 7f6c9e8..ef47179 100644 --- a/red_vision/displays/st7789_pio.py +++ b/red_vision/displays/spi_rp2_pio.py @@ -3,9 +3,9 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# st7789_pio.py +# red_vision/displays/spi_rp2_pio.py # -# OpenCV ST7789 display driver using a PIO interface. Only available on +# Red Vision SPI display driver using a PIO interface. Only available on # Raspberry Pi RP2 processors. # # This class is derived from: @@ -17,28 +17,23 @@ # Copyright (c) 2019 Ivan Belokobylskiy #------------------------------------------------------------------------------- -from .st7789 import ST7789 -from machine import Pin import rp2 +from machine import Pin +from ..utils.pins import save_pin_mode_alt -class ST7789_PIO(ST7789): +class SPI_RP2_PIO(): """ - OpenCV ST7789 display driver using a PIO interface. Only available on + Red Vision SPI display driver using a PIO interface. Only available on Raspberry Pi RP2 processors. """ def __init__( self, - width, - height, sm_id, pin_clk, pin_tx, pin_dc, pin_cs=None, freq=-1, - rotation=0, - bgr_order=True, - reverse_bytes_in_word=True, ): """ Initializes the ST7789 PIO display driver. @@ -72,29 +67,14 @@ def __init__( self._cs = Pin(pin_cs, Pin.OUT, value=1) if pin_cs else None self._freq = freq - # Start the PIO state machine and DMA with 1 bytes per transfer - self._setup_sm_and_dma(1) - - # Call the parent class constructor - super().__init__(width, height, rotation, bgr_order, reverse_bytes_in_word) - - # Change the transfer size to 2 bytes for faster throughput. Can't do 4 - # bytes, because then pairs of pixels get swapped - self._setup_sm_and_dma(2) - - def _setup_sm_and_dma(self, bytes_per_transfer): + def begin(self): """ - Sets up the PIO state machine and DMA for writing to the display. - - Args: - bytes_per_transfer (int): Number of bytes to transfer in each write + Initializes the PIO interface for the display. """ - # Store the bytes per transfer for later use - self._bytes_per_transfer = bytes_per_transfer # Get the current mode and alt of the pins so they can be restored - txMode, txAlt = self._save_pin_mode_alt(self._tx) - clkMode, clkAlt = self._save_pin_mode_alt(self._clk) + txMode, txAlt = save_pin_mode_alt(self._tx) + clkMode, clkAlt = save_pin_mode_alt(self._clk) # Initialize the PIO state machine self._sm = rp2.StateMachine( @@ -103,15 +83,15 @@ def _setup_sm_and_dma(self, bytes_per_transfer): freq = self._freq, out_base = self._tx, sideset_base = self._clk, - pull_thresh = bytes_per_transfer * 8 + pull_thresh = 8 # 8 bits per transfer ) # The tx and clk pins just got their mode and alt set for PIO0 or PIO1. - # We need to save them again to restore later when _write() is called, + # We need to save them again to restore later when write() is called, # if we haven't already if not hasattr(self, '_txMode'): - self._txMode, self._txAlt = self._save_pin_mode_alt(self._tx) - self._clkMode, self._clkAlt = self._save_pin_mode_alt(self._clk) + self._txMode, self._txAlt = save_pin_mode_alt(self._tx) + self._clkMode, self._clkAlt = save_pin_mode_alt(self._clk) # Now restore the original mode and alt of the pins self._tx.init(mode=txMode, alt=txAlt) @@ -124,17 +104,16 @@ def _setup_sm_and_dma(self, bytes_per_transfer): # Configure up DMA to write to the PIO state machine req_num = ((self._sm_id // 4) << 3) + (self._sm_id % 4) dma_ctrl = self._dma.pack_ctrl( - size = {1:0, 2:1, 4:2}[bytes_per_transfer], # 0 = 8-bit, 1 = 16-bit, 2 = 32-bit + size = 0, inc_write = False, treq_sel = req_num, - bswap = False ) self._dma.config( write = self._sm, ctrl = dma_ctrl ) - def _write(self, command=None, data=None): + def write(self, command=None, data=None): """ Writes commands and data to the display. @@ -144,9 +123,9 @@ def _write(self, command=None, data=None): """ # Save the current mode and alt of the spi pins in case they're used by # another device on the same SPI bus - dcMode, dcAlt = self._save_pin_mode_alt(self._dc) - txMode, txAlt = self._save_pin_mode_alt(self._tx) - clkMode, clkAlt = self._save_pin_mode_alt(self._clk) + dcMode, dcAlt = save_pin_mode_alt(self._dc) + txMode, txAlt = save_pin_mode_alt(self._tx) + clkMode, clkAlt = save_pin_mode_alt(self._clk) # Temporarily set the SPI pins to the correct mode and alt for PIO self._dc.init(mode=Pin.OUT) @@ -179,7 +158,7 @@ def _pio_write(self, data): """ # Configure the DMA transfer count and read address count = len(data) if isinstance(data, (bytes, bytearray)) else data.size - self._dma.count = count // self._bytes_per_transfer + self._dma.count = count self._dma.read = data # Start the state machine and DMA transfer, and wait for it to finish diff --git a/red_vision/displays/st7789.py b/red_vision/displays/st7789.py index 80d47bf..aed4f21 100644 --- a/red_vision/displays/st7789.py +++ b/red_vision/displays/st7789.py @@ -3,9 +3,9 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# st7789.py +# red_vision/displays/st7789.py # -# Base class for OpenCV ST7789 display drivers. +# Red Vision ST7789 display driver. # # This class is derived from: # https://github.com/easytarget/st7789-framebuffer/blob/main/st7789_purefb.py @@ -16,13 +16,14 @@ # Copyright (c) 2019 Ivan Belokobylskiy #------------------------------------------------------------------------------- -from .cv2_display import CV2_Display from time import sleep_ms import struct +from ..utils import colors +from .video_display_driver import VideoDisplayDriver -class ST7789(CV2_Display): +class ST7789(VideoDisplayDriver): """ - Base class for OpenCV ST7789 display drivers. + Red Vision ST7789 display driver. """ # ST7789 commands _ST7789_SWRESET = b"\x01" @@ -119,11 +120,12 @@ class ST7789(CV2_Display): def __init__( self, - width, - height, - rotation=0, - bgr_order=True, - reverse_bytes_in_word=True, + interface, + height = None, + width = None, + color_mode = None, + buffer = None, + rotation = 1, ): """ Initializes the ST7789 display driver. @@ -142,9 +144,20 @@ def __init__( reverse_bytes_in_word (bool, optional): - Enable if the display uses LSB byte order for color words """ + self._interface = interface + super().__init__(height, width, color_mode, buffer) + + # Initial rotation + self._rotation = rotation % 4 + + self._interface.begin() # Initial dimensions and offsets; will be overridden when rotation applied - self._width = width - self._height = height + if self._rotation % 2 == 0: + width = self._width + height = self._height + else: + width = self._height + height = self._width self._xstart = 0 self._ystart = 0 # Check display is known and get rotation table @@ -154,20 +167,62 @@ def __init__( [f"{display[0]}x{display[1]}" for display in self._SUPPORTED_DISPLAYS]) raise ValueError( f"Unsupported {width}x{height} display. Supported displays: {supported_displays}") - # Colors - self._bgr_order = bgr_order - self._needs_swap = reverse_bytes_in_word # Reset the display self._soft_reset() # Yes, send init twice, once is not always enough self._send_init(self._ST7789_INIT_CMDS) self._send_init(self._ST7789_INIT_CMDS) - # Initial rotation - self._rotation = rotation % 4 # Apply rotation self._set_rotation(self._rotation) - # Create the framebuffer for the correct rotation - super().__init__((self._height, self._width, 2)) + + def resolution_default(self): + """ + Returns the default resolution for the display. + + Returns: + tuple: (height, width) in pixels + """ + # Use the first supported display as the default + display = self._SUPPORTED_DISPLAYS[0] + return (display[0], display[1]) + + def resolution_is_supported(self, height, width): + """ + Checks if the given resolution is supported by the display. + + Args: + height (int): Height in pixels + width (int): Width in pixels + Returns: + bool: True if the resolution is supported, otherwise False + """ + return any(display[0] == height and display[1] == width for display in self._SUPPORTED_DISPLAYS) + + def color_mode_default(self): + """ + Returns the default color mode for the display. + """ + return colors.COLOR_MODE_BGR565 + + def color_mode_is_supported(self, color_mode): + """ + Checks if the given color mode is supported by the display. + + Args: + color_mode (int): Color mode to check + Returns: + bool: True if the color mode is supported, otherwise False + """ + return color_mode == colors.COLOR_MODE_BGR565 + + def show(self): + """ + Updates the display with the contents of the framebuffer. + """ + # When sending BGR565 pixel data, the ST7789 expects each pair of bytes + # to be sent in the opposite endianness of what the SPI peripheral would + # normally send. So we just swap each pair of bytes. + self._interface.write(None, self._buffer[:,:,::-1]) def _send_init(self, commands): """ @@ -177,14 +232,14 @@ def _send_init(self, commands): commands (list): List of tuples (command, data, delay_ms) """ for command, data, delay_ms in commands: - self._write(command, data) + self._interface.write(command, data) sleep_ms(delay_ms) def _soft_reset(self): """ Sends a software reset command to the display. """ - self._write(self._ST7789_SWRESET) + self._interface.write(self._ST7789_SWRESET) sleep_ms(150) def _find_rotations(self, width, height): @@ -226,47 +281,14 @@ def _set_rotation(self, rotation): self._height, self._xstart, self._ystart, ) = self._rotations[rotation] - if self._bgr_order: - madctl |= self._ST7789_MADCTL_BGR - else: - madctl &= ~self._ST7789_MADCTL_BGR - self._write(self._ST7789_MADCTL, bytes([madctl])) + # Always BGR order for OpenCV + madctl |= self._ST7789_MADCTL_BGR + self._interface.write(self._ST7789_MADCTL, bytes([madctl])) # Set window for writing into - self._write(self._ST7789_CASET, + self._interface.write(self._ST7789_CASET, struct.pack(self._ENCODE_POS, self._xstart, self._width + self._xstart - 1)) - self._write(self._ST7789_RASET, + self._interface.write(self._ST7789_RASET, struct.pack(self._ENCODE_POS, self._ystart, self._height + self._ystart - 1)) - self._write(self._ST7789_RAMWR) + self._interface.write(self._ST7789_RAMWR) # TODO: Can we swap (modify) framebuffer width/height in the super() class? self._rotation = rotation - - def imshow(self, image): - """ - Shows a NumPy image on the display. - - Args: - image (ndarray): Image to show - """ - # Get the common ROI between the image and internal display buffer - image_roi, buffer_roi = self._get_common_roi_with_buffer(image) - - # Ensure the image is in uint8 format - image_roi = self._convert_to_uint8(image_roi) - - # Convert the image to BGR565 format and write it to the buffer - self._convert_to_bgr565(image_roi, buffer_roi) - - # Write buffer to display. Swap bytes if needed - if self._needs_swap: - self._write(None, self._buffer[:, :, ::-1]) - else: - self._write(None, self._buffer) - - def clear(self): - """ - Clears the display by filling it with black color. - """ - # Clear the buffer by filling it with zeros (black) - self._buffer[:] = 0 - # Write the buffer to the display - self._write(None, self._buffer) diff --git a/red_vision/displays/cv2_display.py b/red_vision/displays/video_display.py similarity index 59% rename from red_vision/displays/cv2_display.py rename to red_vision/displays/video_display.py index 2ea1723..5cac326 100644 --- a/red_vision/displays/cv2_display.py +++ b/red_vision/displays/video_display.py @@ -3,28 +3,33 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# cv2_display.py +# red_vision/displays/video_display.py # -# Base class for OpenCV display drivers. +# Red Vision generic display class. This is to be used with `cv.imshow()` in +# place of the window name string used by standard OpenCV. #------------------------------------------------------------------------------- -import cv2 +import cv2 as cv from ulab import numpy as np -from machine import Pin +from ..utils import colors -class CV2_Display(): +class VideoDisplay(): """ - Base class for OpenCV display drivers. + Red Vision generic display class. This is to be used with `cv.imshow()` in + place of the window name string used by standard OpenCV. """ - def __init__(self, buffer_shape): + def __init__( + self, + driver, + ): """ Initializes the display. Args: buffer_shape (tuple): Shape of the buffer as (rows, cols, channels) """ - # Create the frame buffer - self._buffer = np.zeros(buffer_shape, dtype=np.uint8) + # Store driver reference. + self._driver = driver def imshow(self, image): """ @@ -33,13 +38,56 @@ def imshow(self, image): Args: image (ndarray): Image to show """ - raise NotImplementedError("imshow() must be implemented by driver") + # Get the common ROI between the image and internal display buffer. + image_roi, buffer_roi = self._get_common_roi_with_buffer(image) + + # Ensure the image is in uint8 format + image_roi = self._convert_to_uint8(image_roi) + + # Convert the image to current format and write it to the buffer. + color_mode = self._driver.color_mode() + if (color_mode == colors.COLOR_MODE_GRAY8 or + # No conversion available for the modes below, treat as GRAY8 + color_mode == colors.COLOR_MODE_BAYER_BG or + color_mode == colors.COLOR_MODE_BAYER_GB or + color_mode == colors.COLOR_MODE_BAYER_RG or + color_mode == colors.COLOR_MODE_BAYER_GR or + color_mode == colors.COLOR_MODE_BGR233): + self._convert_to_gray8(image_roi, buffer_roi) + elif color_mode == colors.COLOR_MODE_BGR565: + self._convert_to_bgr565(image_roi, buffer_roi) + elif color_mode == colors.COLOR_MODE_BGR888: + self._convert_to_bgr888(image_roi, buffer_roi) + elif color_mode == colors.COLOR_MODE_BGRA8888: + self._convert_to_bgra8888(image_roi, buffer_roi) + else: + raise ValueError("Unsupported color mode") + + # Show the buffer on the display. + self._driver.show() def clear(self): """ Clears the display by filling it with black color. """ - raise NotImplementedError("clear() must be implemented by driver") + self._driver.buffer()[:] = 0 + self._driver.show() + + def splash(self, filename="splash.png"): + """ + Shows a splash image on the display if one is available, otherwise + clears the display of any previous content. + + Args: + filename (str, optional): Path to a splash image file. Defaults to + "splash.png" + """ + try: + # Attempt to load and show the splash image + self.imshow(cv.imread(filename)) + except Exception: + # Couldn't load the image, just clear the display as a fallback + self.clear() def _get_common_roi_with_buffer(self, image): """ @@ -66,10 +114,11 @@ def _get_common_roi_with_buffer(self, image): image_cols = image.shape[1] # Get the common ROI between the image and the buffer - row_max = min(image_rows, self._buffer.shape[0]) - col_max = min(image_cols, self._buffer.shape[1]) + buffer = self._driver.buffer() + row_max = min(image_rows, buffer.shape[0]) + col_max = min(image_cols, buffer.shape[1]) img_roi = image[:row_max, :col_max] - buffer_roi = self._buffer[:row_max, :col_max] + buffer_roi = buffer[:row_max, :col_max] return img_roi, buffer_roi def _convert_to_uint8(self, image): @@ -89,15 +138,15 @@ def _convert_to_uint8(self, image): # Convert to uint8 format. This unfortunately requires creating a new # buffer for the converted image, which takes more memory if image.dtype == np.int8: - return cv2.convertScaleAbs(image, alpha=1, beta=127) + return cv.convertScaleAbs(image, alpha=1, beta=127) elif image.dtype == np.int16: - return cv2.convertScaleAbs(image, alpha=1/255, beta=127) + return cv.convertScaleAbs(image, alpha=1/255, beta=127) elif image.dtype == np.uint16: - return cv2.convertScaleAbs(image, alpha=1/255) + return cv.convertScaleAbs(image, alpha=1/255) elif image.dtype == np.float: # This implementation creates an additional buffer from np.clip() # TODO: Find another solution that avoids an additional buffer - return cv2.convertScaleAbs(np.clip(image, 0, 1), alpha=255) + return cv.convertScaleAbs(np.clip(image, 0, 1), alpha=255) else: raise ValueError(f"Unsupported image dtype: {image.dtype}") @@ -122,11 +171,11 @@ def _convert_to_gray8(self, src, dst): # https://github.com/v923z/micropython-ulab/issues/726 dst[:] = src.reshape(dst.shape) elif ch == 2: # BGR565 - dst = cv2.cvtColor(src, cv2.COLOR_BGR5652GRAY, dst) + dst = cv.cvtColor(src, cv.COLOR_BGR5652GRAY, dst) elif ch == 3: # BGR888 - dst = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY, dst) + dst = cv.cvtColor(src, cv.COLOR_BGR2GRAY, dst) elif ch == 4: # BGRA8888 - dst = cv2.cvtColor(src, cv2.COLOR_BGRA2GRAY, dst) + dst = cv.cvtColor(src, cv.COLOR_BGRA2GRAY, dst) else: raise ValueError("Unsupported number of channels in source image") @@ -146,16 +195,45 @@ def _convert_to_bgr565(self, src, dst): # Convert the image to BGR565 format based on the number of channels if ch == 1: # GRAY8 - dst = cv2.cvtColor(src, cv2.COLOR_GRAY2BGR565, dst) + dst = cv.cvtColor(src, cv.COLOR_GRAY2BGR565, dst) elif ch == 2: # BGR565 # Already in BGR565 format # For some reason, this is relatively slow and creates a new buffer: # https://github.com/v923z/micropython-ulab/issues/726 dst[:] = src.reshape(dst.shape) elif ch == 3: # BGR888 - dst = cv2.cvtColor(src, cv2.COLOR_BGR2BGR565, dst) + dst = cv.cvtColor(src, cv.COLOR_BGR2BGR565, dst) elif ch == 4: # BGRA8888 - dst = cv2.cvtColor(src, cv2.COLOR_BGRA2BGR565, dst) + dst = cv.cvtColor(src, cv.COLOR_BGRA2BGR565, dst) + else: + raise ValueError("Unsupported number of channels in source image") + + def _convert_to_bgr888(self, src, dst): + """ + Converts an image to BGR888 format. + + Args: + src (ndarray): Input image + dst (ndarray): Output BGR888 buffer + """ + # Determine the number of channels in the image + if src.ndim < 3: + ch = 1 + else: + ch = src.shape[2] + + # Convert the image to BGR888 format based on the number of channels + if ch == 1: # GRAY8 + dst = cv.cvtColor(src, cv.COLOR_GRAY2BGR, dst) + elif ch == 2: # BGR565 + dst = cv.cvtColor(src, cv.COLOR_BGR5652BGR, dst) + elif ch == 3: # BGR888 + # Already in BGR888 format + # For some reason, this is relatively slow and creates a new buffer: + # https://github.com/v923z/micropython-ulab/issues/726 + dst[:] = src.reshape(dst.shape) + elif ch == 4: # BGRA8888 + dst = cv.cvtColor(src, cv.COLOR_BGRA2BGR, dst) else: raise ValueError("Unsupported number of channels in source image") @@ -175,11 +253,11 @@ def _convert_to_bgra8888(self, src, dst): # Convert the image to BGRA8888 format based on the number of channels if ch == 1: # GRAY8 - dst = cv2.cvtColor(src, cv2.COLOR_GRAY2BGRA, dst) + dst = cv.cvtColor(src, cv.COLOR_GRAY2BGRA, dst) elif ch == 2: # BGR565 - dst = cv2.cvtColor(src, cv2.COLOR_BGR5652BGRA, dst) + dst = cv.cvtColor(src, cv.COLOR_BGR5652BGRA, dst) elif ch == 3: # BGR888 - dst = cv2.cvtColor(src, cv2.COLOR_BGR2BGRA, dst) + dst = cv.cvtColor(src, cv.COLOR_BGR2BGRA, dst) elif ch == 4: # BGRA8888 # Already in BGRA8888 format # For some reason, this is relatively slow and creates a new buffer: @@ -187,68 +265,3 @@ def _convert_to_bgra8888(self, src, dst): dst[:] = src.reshape(dst.shape) else: raise ValueError("Unsupported number of channels in source image") - - def _save_pin_mode_alt(self, pin): - """ - Saves the current `mode` and `alt` of the pin so it can be restored - later. Mostly used for SPI displays on a shared SPI bus with a driver - that needs non-SPI pin modes, such as the RP2 PIO driver. This allows - other devices on the bus to continue using the SPI interface after the - display driver finishes communicating with the display. - - Returns: - tuple: (mode, alt) - """ - # See: https://github.com/micropython/micropython/issues/17515 - # There's no way to get the mode and alt of a pin directly, so we - # convert the pin to a string and parse it. Example formats: - # "Pin(GPIO16, mode=OUT)" - # "Pin(GPIO16, mode=ALT, alt=SPI)" - pin_str = str(pin) - - # Extract the "mode" parameter from the pin string - try: - # Split between "mode=" and the next comma or closing parenthesis - mode_str = pin_str[pin_str.index("mode=") + 5:].partition(",")[0].partition(")")[0] - - # Look up the mode in Pin class dictionary - mode = Pin.__dict__[mode_str] - except (ValueError, KeyError): - # No mode specified, just set to -1 (default) - mode = -1 - - # Extrct the "alt" parameter from the pin string - try: - # Split between "alt=" and the next comma or closing parenthesis - alt_str = pin_str[pin_str.index("alt=") + 4:].partition(",")[0].partition(")")[0] - - # Sometimes the value comes back as a number instead of a valid - # "ALT_xyz" string, so we need to check it - if "ALT_" + alt_str in Pin.__dict__: - # Look up the alt in Pin class dictionary (with "ALT_" prefix) - alt = Pin.__dict__["ALT_" + alt_str] - else: - # Convert the altStr to an integer - alt = int(alt_str) - except (ValueError, KeyError): - # No alt specified, just set to -1 (default) - alt = -1 - - # Return the mode and alt as a tuple - return (mode, alt) - - def splash(self, filename="splash.png"): - """ - Shows a splash image on the display if one is available, otherwise - clears the display of any previous content. - - Args: - filename (str, optional): Path to a splash image file. Defaults to - "splash.png" - """ - try: - # Attempt to load and show the splash image - self.imshow(cv2.imread(filename)) - except Exception: - # Couldn't load the image, just clear the display as a fallback - self.clear() diff --git a/red_vision/displays/video_display_driver.py b/red_vision/displays/video_display_driver.py new file mode 100644 index 0000000..28d7845 --- /dev/null +++ b/red_vision/displays/video_display_driver.py @@ -0,0 +1,21 @@ +#------------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 SparkFun Electronics +#------------------------------------------------------------------------------- +# red_vision/utils/video_display_driver.py +# +# Red Vision abstract base class for display drivers. +#------------------------------------------------------------------------------- + +from ..utils.video_driver import VideoDriver + +class VideoDisplayDriver(VideoDriver): + """ + Red Vision abstract base class for display drivers. + """ + def show(self): + """ + Updates the display with the contents of the framebuffer. + """ + raise NotImplementedError("Subclass must implement this method") diff --git a/red_vision/touch_screens/__init__.py b/red_vision/touch_screens/__init__.py index 48e2e4e..60cdcda 100644 --- a/red_vision/touch_screens/__init__.py +++ b/red_vision/touch_screens/__init__.py @@ -3,10 +3,10 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# cv2_drivers/touch_screens/__init__.py +# red_vision/touch_screens/__init__.py # -# Imports all available touch screen drivers for MicroPython OpenCV. +# Imports all available Red Vision touch screen drivers. #------------------------------------------------------------------------------- -# Import platform agnostic drivers -from . import cst816 +# Import platform agnostic drivers. +from .cst816 import CST816 diff --git a/red_vision/touch_screens/cst816.py b/red_vision/touch_screens/cst816.py index 8b56448..e6c6e69 100644 --- a/red_vision/touch_screens/cst816.py +++ b/red_vision/touch_screens/cst816.py @@ -3,9 +3,9 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# st7789.py +# red_vision/touch_screens/cst816.py # -# Base class for OpenCV ST7789 display drivers. +# Red Vision CST816 touch screen driver. # # This class is derived from: # https://github.com/fbiego/CST816S @@ -13,11 +13,9 @@ # Copyright (c) 2021 Felix Biego #------------------------------------------------------------------------------- -from .cv2_touch_screen import CV2_Touch_Screen - -class CST816(CV2_Touch_Screen): +class CST816(): """ - OpenCV CST816 touch screen driver using an I2C interface. + Red Vision CST816 touch screen driver. """ _I2C_ADDRESS = 0x15 _CHIP_ID = 0xB6 @@ -108,7 +106,7 @@ def _get_chip_id(self): Returns: int: The chip ID of the HM01B0 (should be 0xB6). """ - return self.read_register_value(self._REG_CHIP_ID) + return self._read_register_value(self._REG_CHIP_ID) def is_touched(self): """ @@ -118,7 +116,7 @@ def is_touched(self): bool: True if touching, False otherwise """ # Read the number of touches - touch_num = self.read_register_value(self._REG_FINGER_NUM) + touch_num = self._read_register_value(self._REG_FINGER_NUM) # If there are any touches, return True return touch_num > 0 @@ -131,8 +129,8 @@ def get_touch_xy(self): Returns: tuple: (x, y) coordinates of the touch point """ - x = self.read_register_value(self._REG_X_POS_H, 2) & 0x0FFF - y = self.read_register_value(self._REG_Y_POS_H, 2) & 0x0FFF + x = self._read_register_value(self._REG_X_POS_H, 2) & 0x0FFF + y = self._read_register_value(self._REG_Y_POS_H, 2) & 0x0FFF # Adjust for the rotation if self.rotation == 0: @@ -146,7 +144,7 @@ def get_touch_xy(self): return (x, y) - def read_register_value(self, reg, num_bytes=1): + def _read_register_value(self, reg, num_bytes=1): """ Read a single byte from the specified register. diff --git a/red_vision/touch_screens/cv2_touch_screen.py b/red_vision/touch_screens/cv2_touch_screen.py deleted file mode 100644 index 439a314..0000000 --- a/red_vision/touch_screens/cv2_touch_screen.py +++ /dev/null @@ -1,21 +0,0 @@ -#------------------------------------------------------------------------------- -# SPDX-License-Identifier: MIT -# -# Copyright (c) 2025 SparkFun Electronics -#------------------------------------------------------------------------------- -# cv2_touch_screen.py -# -# Base class for OpenCV touch screen drivers. -#------------------------------------------------------------------------------- - -class CV2_Touch_Screen(): - """ - Base class for OpenCV touch screen drivers. - """ - def __init__(self): - """ - Initializes the touch screen. - """ - pass - - # TODO: Implement common methods for all touch screens diff --git a/red_vision/utils/colors.py b/red_vision/utils/colors.py new file mode 100644 index 0000000..6b60e59 --- /dev/null +++ b/red_vision/utils/colors.py @@ -0,0 +1,40 @@ +#------------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 SparkFun Electronics +#------------------------------------------------------------------------------- +# red_vision/utils/colors.py +# +# Red Vision color mode constants and utility functions. +#------------------------------------------------------------------------------- + +# Color mode constants. +COLOR_MODE_BAYER_BG = 0 +COLOR_MODE_BAYER_GB = 1 +COLOR_MODE_BAYER_RG = 2 +COLOR_MODE_BAYER_GR = 3 +COLOR_MODE_GRAY8 = 4 +COLOR_MODE_BGR233 = 5 +COLOR_MODE_BGR565 = 6 +COLOR_MODE_BGR888 = 7 +COLOR_MODE_BGRA8888 = 8 + +def bytes_per_pixel(color_mode): + """ + Returns the number of bytes per pixel for the given color mode. + """ + if (color_mode == COLOR_MODE_BAYER_BG or + color_mode == COLOR_MODE_BAYER_GB or + color_mode == COLOR_MODE_BAYER_RG or + color_mode == COLOR_MODE_BAYER_GR or + color_mode == COLOR_MODE_GRAY8 or + color_mode == COLOR_MODE_BGR233): + return 1 + elif color_mode == COLOR_MODE_BGR565: + return 2 + elif color_mode == COLOR_MODE_BGR888: + return 3 + elif color_mode == COLOR_MODE_BGRA8888: + return 4 + else: + raise ValueError("Unsupported color mode") diff --git a/red_vision/utils/memory.py b/red_vision/utils/memory.py new file mode 100644 index 0000000..902deae --- /dev/null +++ b/red_vision/utils/memory.py @@ -0,0 +1,66 @@ +#------------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 SparkFun Electronics +#------------------------------------------------------------------------------- +# red_vision/utils/memory.py +# +# Red Vision memory utility functions. +#------------------------------------------------------------------------------- + +import sys +import machine +import uctypes + +def is_in_internal_ram(address): + """ + Checks whether a given object or memory address is in internal RAM. + """ + # Get the memory address if an object is given. + if type(address) is not int: + address = uctypes.addressof(address) + + if "rp2" in sys.platform: + # SRAM address range. + SRAM_BASE = 0x20000000 + SRAM_END = 0x20082000 + + # Return whether address is in SRAM. + return address >= SRAM_BASE and address < SRAM_END + else: + raise NotImplementedError("Not implemented for this platform.") + +def is_in_external_ram(address): + """ + Checks whether a given object or memory address is in external RAM. + """ + return not is_in_internal_ram(address) + +def external_ram_max_bytes_per_second(): + """ + Estimates the maximum bytes per second for external RAM access. + """ + if "rp2" in sys.platform: + # PSRAM timing register parameters. + XIP_QMI_BASE = 0x400D0000 + M1_TIMING = XIP_QMI_BASE + 0x20 + CLKDIV_MASK = 0xFF + + # Get PSRAM clock divider, typically 2. + psram_clk_div = machine.mem32[M1_TIMING] & CLKDIV_MASK + + # Compute PSRAM pixel transfer rate. PSRAM is on the QSPI bus, which + # transfers 1 byte every 2 clock cycles. + psram_clock_hz = machine.freq() / psram_clk_div # Typically 75 MHz + psram_bytes_per_second = psram_clock_hz / 2 # Typically 37.5 MBps + + # Probing with an oscilloscope has shown that the XIP stream typically + # performs transfers in 32 bit bursts every 19 system clock cycles + # (~127ns) instead of the nominal 16 system clock cycles (~107ns). We'll + # include it as a safety margin. + psram_bytes_per_second *= 16 / 19 # Typically 31.5 MBps + + # Return the estimated PSRAM bytes per second. + return psram_bytes_per_second + else: + raise NotImplementedError("Not implemented for this platform.") diff --git a/red_vision/utils/pins.py b/red_vision/utils/pins.py new file mode 100644 index 0000000..cfd39fc --- /dev/null +++ b/red_vision/utils/pins.py @@ -0,0 +1,61 @@ +#------------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 SparkFun Electronics +#------------------------------------------------------------------------------- +# red_vision/utils/pins.py +# +# Red Vision Pin utility functions. +#------------------------------------------------------------------------------- + +from machine import Pin + +def save_pin_mode_alt(pin): + """ + Saves the current `mode` and `alt` of the pin so it can be restored + later. Mostly used for SPI displays on a shared SPI bus with a driver + that needs non-SPI pin modes, such as the RP2 PIO driver. This allows + other devices on the bus to continue using the SPI interface after the + display driver finishes communicating with the display. + + Returns: + tuple: (mode, alt) + """ + # See: https://github.com/micropython/micropython/issues/17515 + # There's no way to get the mode and alt of a pin directly, so we + # convert the pin to a string and parse it. Example formats: + # "Pin(GPIO16, mode=OUT)" + # "Pin(GPIO16, mode=ALT, alt=SPI)" + pin_str = str(pin) + + # Extract the "mode" parameter from the pin string + try: + # Split between "mode=" and the next comma or closing parenthesis + mode_str = pin_str[pin_str.index("mode=") + 5:].partition(",")[0].partition(")")[0] + + # Look up the mode in Pin class dictionary + mode = Pin.__dict__[mode_str] + except (ValueError, KeyError): + # No mode specified, just set to -1 (default) + mode = -1 + + # Extrct the "alt" parameter from the pin string + try: + # Split between "alt=" and the next comma or closing parenthesis + alt_str = pin_str[pin_str.index("alt=") + 4:].partition(",")[0].partition(")")[0] + + # Sometimes the value comes back as a number instead of a valid + # "ALT_xyz" string, so we need to check it + if "ALT_" + alt_str in Pin.__dict__: + # Look up the alt in Pin class dictionary (with "ALT_" prefix) + alt = Pin.__dict__["ALT_" + alt_str] + else: + # Convert the altStr to an integer + alt = int(alt_str) + except (ValueError, KeyError): + # No alt specified, just set to -1 (default) + alt = -1 + + # Return the mode and alt as a tuple + return (mode, alt) + diff --git a/red_vision/utils/video_driver.py b/red_vision/utils/video_driver.py new file mode 100644 index 0000000..30bfaf9 --- /dev/null +++ b/red_vision/utils/video_driver.py @@ -0,0 +1,118 @@ +#------------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 SparkFun Electronics +#------------------------------------------------------------------------------- +# red_vision/utils/video_driver.py +# +# Red Vision abstract base class for camera and display drivers. +#------------------------------------------------------------------------------- + +from ulab import numpy as np +from . import colors + +class VideoDriver(): + """ + Red Vision abstract base class for camera and display drivers. + """ + def __init__( + self, + height = None, + width = None, + color_mode = None, + buffer = None, + ): + """ + Initializes the camera. + """ + # Determine image resolution. + if height is None or width is None: + # Use the driver's default resolution. + self._height, self._width = self.resolution_default() + else: + # Check if the driver supports the requested resolution. + if not self.resolution_is_supported(height, width): + raise ValueError("Unsupported resolution") + + # Store the resolution. + self._height = height + self._width = width + + # Determine color mode. + if color_mode is None: + # Use the driver's default color mode. + self._color_mode = self.color_mode_default() + else: + # Check if the driver supports the requested color mode. + if not self.color_mode_is_supported(color_mode): + raise ValueError("Unsupported color mode") + + # Store the color mode. + self._color_mode = color_mode + + # Create or store the image buffer. + self._bytes_per_pixel = colors.bytes_per_pixel(self._color_mode) + buffer_shape = (self._height, self._width, self._bytes_per_pixel) + if buffer is None: + # No buffer provided, create a new one. + self._buffer = np.zeros(buffer_shape, dtype=np.uint8) + else: + # Use the provided buffer, formatted as a NumPy ndarray. + self._buffer = np.frombuffer(buffer, dtype=np.uint8) + + # Reshape to the provided dimensions. + self._buffer = self._buffer.reshape(buffer_shape) + + def buffer(self): + """ + Returns the framebuffer used by the display. + + Returns: + ndarray: Framebuffer + """ + return self._buffer + + def color_mode(self): + """ + Returns the current color mode of the display. + """ + return self._color_mode + + def resolution_default(): + """ + Returns the default resolution of the camera. + + Returns: + tuple: (height, width) + """ + raise NotImplementedError("Subclass must implement this method") + + def resolution_is_supported(): + """ + Returns whether the given resolution is supported by the camera. + + Args: + height (int): Image height + width (int): Image width + Returns: + bool: True if supported, False otherwise + """ + raise NotImplementedError("Subclass must implement this method") + + def color_mode_default(): + """ + Returns the default color mode of the camera. + + Returns: + int: Color mode + """ + raise NotImplementedError("Subclass must implement this method") + + def color_mode_is_supported(): + """ + Returns the default resolution of the camera. + + Returns: + tuple: (height, width) + """ + raise NotImplementedError("Subclass must implement this method") diff --git a/red_vision_examples/dvi_examples/ex01_hello_dvi.py b/red_vision_examples/dvi_examples/ex01_hello_dvi.py index 304a603..fd78c06 100644 --- a/red_vision_examples/dvi_examples/ex01_hello_dvi.py +++ b/red_vision_examples/dvi_examples/ex01_hello_dvi.py @@ -3,7 +3,7 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# ex01_hello_dvi.py +# red_vision_examples/dvi_examples/ex01_hello_dvi.py # # This example can be used to verify that DVI output is functioning correctly # on your board. It creates a simple test image with various colors and shapes, @@ -11,9 +11,8 @@ #------------------------------------------------------------------------------- # This example does not use the `rv_init` module, in order to demonstrate some -# more advanced features of the DVI display driver. So we instead import the -# display driver here. -from red_vision.displays import dvi_rp2_hstx +# more advanced features. The initialization is done directly in this example. +import red_vision as rv # Import OpenCV and NumPy. import cv2 as cv @@ -25,18 +24,23 @@ width = 320 height = 240 -# Create the singleston DVI_HSTX display instance. -display = dvi_rp2_hstx.DVI_HSTX( - width = width, - height = height, +# 4 different color modes are supported, though not all color modes can be +# used with all resolutions due to memory constraints. +# color_mode = rv.colors.COLOR_MODE_BGR233 +# color_mode = rv.colors.COLOR_MODE_GRAY8 +color_mode = rv.colors.COLOR_MODE_BGR565 +# color_mode = rv.colors.COLOR_MODE_BGRA8888 - # 4 different color modes are supported, though not all color modes can be - # used with all resolutions due to memory constraints. - # color_mode = dvi_rp2_hstx.DVI_HSTX.COLOR_BGR233, - # color_mode = dvi_rp2_hstx.DVI_HSTX.COLOR_GRAY8, - color_mode = dvi_rp2_hstx.DVI_HSTX.COLOR_BGR565, - # color_mode = dvi_rp2_hstx.DVI_HSTX.COLOR_BGRA8888, +# Create a buffer for the display to use. This is not usually necessary, but it +# allows us to directly mofify the display buffer later for demonstration. +bytes_per_pixel = rv.colors.bytes_per_pixel(color_mode) +buffer = np.zeros( + (height, width, bytes_per_pixel), + dtype = np.uint8 +) +# Create the HSTX interface. +interface = rv.displays.DVI_RP2_HSTX( # Pins default to the SparkFun HSTX to DVI Breakout: # https://www.sparkfun.com/sparkfun-hstx-to-dvi-breakout.html # pin_clk_p = 14, @@ -46,12 +50,30 @@ # pin_d1_p = 16, # pin_d1_n = 17, # pin_d2_p = 12, - # pin_d2_n = 13 + # pin_d2_n = 13, ) +# Initialize the DVI driver. +driver = rv.displays.DVI( + interface = interface, + + # Optionally specify the image resolution. + height = height, + width = width, + + # Optionally specify the image color mode. + color_mode = color_mode, + + # Optionally specify the image buffer to use. + buffer = buffer, +) + +# Create the VideoDisplay object. +display = rv.displays.VideoDisplay(driver) + # OpenCV doesn't have a BGR233 color conversion, so if we're using that mode, # we need to create our test image with single channel values. -if display._color_mode == dvi_rp2_hstx.DVI_HSTX.COLOR_BGR233: +if driver.color_mode() == rv.colors.COLOR_MODE_BGR233: image_channels = 1 # BGR233 packs each byte as follows: RRRGGGBB color_red = (0xE0) @@ -96,28 +118,11 @@ # Draw a color gradient test pattern directly into the display buffer. for i in range(256): - display._buffer[0:10, width - 256 + i] = i + buffer[0:10, width - 256 + i] = i -# When writing the display buffer directly, if its buffer is in PSRAM, some -# pixels may not update until a garbage collection cycle occurs. This is because -# the DVI driver uses the XIP streaming interface to read directly from PSRAM, -# which bypasses the XIP cache. -if display._buffer_is_in_psram: +# If the display buffer is in PSRAM, some pixels may not update until a garbage +# collection occurs. This is because the DVI driver uses the XIP streaming +# interface to read directly from PSRAM, which bypasses the XIP cache. +if rv.utils.memory.is_in_external_ram(buffer): import gc gc.collect() - -# If the ST7789 display is also connected, it can be controlled independently -# of the DVI display. For example, we can show a splash screen on it: -from red_vision.displays import st7789_pio -spi = machine.SPI(baudrate=24_000_000) -display2 = st7789_pio.ST7789_PIO( - width = 240, - height = 320, - sm_id = 4, - pin_clk = 22, - pin_tx = 23, - pin_dc = 20, - pin_cs = 21, - rotation = 1 -) -display2.splash("red_vision_examples/images/splash.png") diff --git a/red_vision_examples/dvi_examples/ex02_high_fps_camera.py b/red_vision_examples/dvi_examples/ex02_high_fps_camera.py index 3995451..91b45ee 100644 --- a/red_vision_examples/dvi_examples/ex02_high_fps_camera.py +++ b/red_vision_examples/dvi_examples/ex02_high_fps_camera.py @@ -3,7 +3,7 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# ex02_high_fps_camera.py +# red_vision_examples/dvi_examples/ex02_high_fps_camera.py # # This example demsontrates how to show a high frame rate camera stream on a DVI # display. This only works with cameras that support the exact same resolutions @@ -23,25 +23,22 @@ #------------------------------------------------------------------------------- # This example does not use the `rv_init` module, in order to demonstrate some -# more advanced features of the drivers. So we instead import the drivers here. -from red_vision.displays import dvi_rp2_hstx -from red_vision.cameras import ov5640_pio +# more advanced features. The initialization is done directly in this example. +import red_vision as rv # Import NumPy. from ulab import numpy as np -# Import addressof from uctypes. -from uctypes import addressof - # Import machine and rp2 for I2C and PIO. import machine import rp2 # Image size and bytes per pixel (depends on color mode). This example defaults -# to 320x240 with BGR565 (2 bytes per pixel). +# to 320x240 with BGR565. width = 320 height = 240 -bytes_per_pixel = 2 +color_mode = rv.colors.COLOR_MODE_BGR565 +bytes_per_pixel = rv.colors.bytes_per_pixel(color_mode) # Create the image buffer to be shared between the camera and display. buffer = np.zeros( @@ -52,36 +49,79 @@ # Verify that the buffer is located in SRAM. If it's in external PSRAM, it # probably won't work due to the QSPI bus becoming bottlenecked by both the # camera and display trying to access it at the same time. -SRAM_BASE = 0x20000000 -SRAM_END = 0x20082000 -buffer_addr = addressof(buffer) -if buffer_addr < SRAM_BASE or buffer_addr >= SRAM_END: - raise MemoryError("Buffer is not located in SRAM") - -# Initialize the DVI display, using the shared buffer. -display = dvi_rp2_hstx.DVI_HSTX( - width = width, +if rv.utils.memory.is_in_external_ram(buffer): + raise MemoryError("Buffer must be in internal RAM for this example") + +# Create the HSTX interface. +interface = rv.displays.DVI_RP2_HSTX( + # Pins default to the SparkFun HSTX to DVI Breakout: + # https://www.sparkfun.com/sparkfun-hstx-to-dvi-breakout.html + # pin_clk_p = 14, + # pin_clk_n = 15, + # pin_d0_p = 18, + # pin_d0_n = 19, + # pin_d1_p = 16, + # pin_d1_n = 17, + # pin_d2_p = 12, + # pin_d2_n = 13, +) + +# Initialize the DVI driver using the shared buffer. +driver = rv.displays.DVI( + interface = interface, + + # Optionally specify the image resolution. height = height, - color_mode = dvi_rp2_hstx.DVI_HSTX.COLOR_BGR565, + width = width, + + # Optionally specify the image color mode. + color_mode = color_mode, + + # Optionally specify the image buffer to use. buffer = buffer, ) +# Create the VideoDisplay object. +display = rv.displays.VideoDisplay(driver) + # Initialize the OV5640 camera, using the shared buffer. i2c = machine.I2C() rp2.PIO(1).gpio_base(16) -camera = ov5640_pio.OV5640_PIO( - i2c, + +# Create the PIO interface. +interface = rv.cameras.DVP_RP2_PIO( sm_id = 5, pin_d0 = 28, pin_vsync = 42, pin_hsync = 41, pin_pclk = 40, - pin_xclk = 44, # Optional xclock pin, specify if needed - xclk_freq = 20_000_000, - buffer = buffer, + + # Optionally specify the XCLK pin if needed by your camera. + pin_xclk = 44, +) + +# Initialize the OV5640 driver using the shared buffer. +driver = rv.cameras.OV5640( + interface, + i2c, + + # Optionally run in continuous capture mode. continuous = True, + + # Optionally specify the image resolution. + height = height, + width = width, + + # Optionally specify the image color mode. + color_mode = color_mode, + + # Optionally specify the image buffer to use. + buffer = buffer, ) +# Create the VideoCapture object. +camera = rv.cameras.VideoCapture(driver) + # Open the camera to start the continuous capture process. camera.open() -camera._capture() +camera.grab() diff --git a/red_vision_examples/ex01_hello_opencv.py b/red_vision_examples/ex01_hello_opencv.py index 4d264e7..de91228 100644 --- a/red_vision_examples/ex01_hello_opencv.py +++ b/red_vision_examples/ex01_hello_opencv.py @@ -3,7 +3,7 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# ex01_hello_opencv.py +# red_vision_examples/ex01_hello_opencv.py # # This example demonstrates near-minimal code to get started with OpenCV in # MicroPython. It can be used to verify that OpenCV is working correctly, and diff --git a/red_vision_examples/ex02_camera.py b/red_vision_examples/ex02_camera.py index e5d7863..1893321 100644 --- a/red_vision_examples/ex02_camera.py +++ b/red_vision_examples/ex02_camera.py @@ -3,7 +3,7 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# ex02_camera.py +# red_vision_examples/ex02_camera.py # # This example demonstrates how to read frames from a camera and show them on a # display using OpenCV in MicroPython. It can be used to verify that the camera diff --git a/red_vision_examples/ex03_touch_screen.py b/red_vision_examples/ex03_touch_screen.py index 0cb69c9..c4080d3 100644 --- a/red_vision_examples/ex03_touch_screen.py +++ b/red_vision_examples/ex03_touch_screen.py @@ -3,7 +3,7 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# ex03_touch_screen.py +# red_vision_examples/ex03_touch_screen.py # # This example demonstrates how to read input from a touch screen, which can be # used to verify that the touch screen driver is functioning. It simply draws diff --git a/red_vision_examples/ex04_imread_imwrite.py b/red_vision_examples/ex04_imread_imwrite.py index b809782..ae963d0 100644 --- a/red_vision_examples/ex04_imread_imwrite.py +++ b/red_vision_examples/ex04_imread_imwrite.py @@ -3,7 +3,7 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# ex04_imread_imwrite.py +# red_vision_examples/ex04_imread_imwrite.py # # This example demonstrates how to read and write images to and from the # MicroPython filesystem using `cv.imread()` and `cv.imwrite()`. Any paths diff --git a/red_vision_examples/ex05_performance.py b/red_vision_examples/ex05_performance.py index bc71b4e..230f40e 100644 --- a/red_vision_examples/ex05_performance.py +++ b/red_vision_examples/ex05_performance.py @@ -3,7 +3,7 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# ex05_performance.py +# red_vision_examples/ex05_performance.py # # This example demonstrates some performance optimization techniques, and ways # to measure performance in the MicroPython port of OpenCV. Read through the diff --git a/red_vision_examples/ex06_detect_sfe_logo.py b/red_vision_examples/ex06_detect_sfe_logo.py index a8eef72..741fbb2 100644 --- a/red_vision_examples/ex06_detect_sfe_logo.py +++ b/red_vision_examples/ex06_detect_sfe_logo.py @@ -3,7 +3,7 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# ex06_detect_sfe_logo.py +# red_vision_examples/ex06_detect_sfe_logo.py # # This example demonstrates a basic vision processing pipeline. A pipeline is # just a sequence of steps used to extract meaningful data from an image. The diff --git a/red_vision_examples/ex07_animation.py b/red_vision_examples/ex07_animation.py index b23879a..199cec2 100644 --- a/red_vision_examples/ex07_animation.py +++ b/red_vision_examples/ex07_animation.py @@ -3,7 +3,7 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# ex07_animation.py +# red_vision_examples/ex07_animation.py # # This example demonstrates how to play an animation using a series of frames # stored in a single image file. It assumes full 320x240 frames are stacked diff --git a/red_vision_examples/rv_init/__init__.py b/red_vision_examples/rv_init/__init__.py index e9a5350..1998b3d 100644 --- a/red_vision_examples/rv_init/__init__.py +++ b/red_vision_examples/rv_init/__init__.py @@ -1,19 +1,21 @@ -# Initializes various hardware components for OpenCV in MicroPython. The +#------------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 SparkFun Electronics +#------------------------------------------------------------------------------- +# red_vision_examples/rv_init/__init__.py +# +# This example module initializes various Red Vision hardware components. The # examples import this module, but you could instead create/edit a `boot.py` # script to automatically initialize the hardware when the board boots up. See: # https://micropython.org/resources/docs/en/latest/reference/reset_boot.html#id4 +#------------------------------------------------------------------------------- -# Import the display driver -try: - from .display import display -except: - print("Display initialization failed, skipping...") - -# Optional - Show a splash screen on the display with an optional filename (if -# not provided, it defaults to `splash.png` in the root directory of the -# MicroPython filesystem). If the file is not present, the driver will simply -# clear the display of any previous content -display.splash("red_vision_examples/images/splash.png") +# When the Red Vision Kit for RedBoard is used with the IoT RedBoard RP2350, +# both the display and camera use GPIO 16-47 instead of GPIO 0-31, so we need to +# adjust the base GPIO for PIO drivers +import rp2 +rp2.PIO(1).gpio_base(16) # Import the camera driver try: @@ -21,6 +23,18 @@ except: print("Camera initialization failed, skipping...") +# Import the display driver +try: + from .display import display + + # Optional - Show a splash screen on the display with an optional filename + # (if not provided, it defaults to `splash.png` in the root directory of the + # MicroPython filesystem). If the file is not present, the driver will + # simply clear the display of any previous content + display.splash("red_vision_examples/images/splash.png") +except: + print("Display initialization failed, skipping...") + # Import the touch screen driver try: from .touch_screen import touch_screen diff --git a/red_vision_examples/rv_init/bus_i2c.py b/red_vision_examples/rv_init/bus_i2c.py index 6fcd370..2521e7a 100644 --- a/red_vision_examples/rv_init/bus_i2c.py +++ b/red_vision_examples/rv_init/bus_i2c.py @@ -1,3 +1,13 @@ +#------------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 SparkFun Electronics +#------------------------------------------------------------------------------- +# red_vision_examples/rv_init/bus_i2c.py +# +# This example module initializes an I2C bus for use with other devices. +#------------------------------------------------------------------------------- + # Import the machine.I2C class from machine import I2C @@ -7,5 +17,5 @@ # id = 0, # sda = 0, # scl = 1, - # freq = 400_000 + # freq = 100_000 ) diff --git a/red_vision_examples/rv_init/bus_spi.py b/red_vision_examples/rv_init/bus_spi.py index 76ff801..6ef07e7 100644 --- a/red_vision_examples/rv_init/bus_spi.py +++ b/red_vision_examples/rv_init/bus_spi.py @@ -1,3 +1,13 @@ +#------------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 SparkFun Electronics +#------------------------------------------------------------------------------- +# red_vision_examples/rv_init/bus_spi.py +# +# This example module initializes an SPI bus for use with other devices. +#------------------------------------------------------------------------------- + # Import the machine.SPI class from machine import SPI @@ -9,5 +19,4 @@ # sck = 2, # mosi = 3, # miso = 4, - # freq = 100_000 ) diff --git a/red_vision_examples/rv_init/camera.py b/red_vision_examples/rv_init/camera.py index e978b38..2ff7cd3 100644 --- a/red_vision_examples/rv_init/camera.py +++ b/red_vision_examples/rv_init/camera.py @@ -1,40 +1,88 @@ -# Initializes a camera object. Multiple options are provided below, so you can -# choose one that best fits your needs. You may need to adjust the arguments -# based on your specific camera and board configuration +#------------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 SparkFun Electronics +#------------------------------------------------------------------------------- +# red_vision_examples/rv_init/camera.py +# +# This example module initializes a Red Vision camera object. Multiple drivers +# and interfaces are provided for various devices, so you can uncomment whatever +# best fits your needs. You may need to adjust the arguments based on your +# specific camera and board configuration. The actual camera object is created +# at the end of the file. +#------------------------------------------------------------------------------- -# Import the OpenCV camera drivers -from red_vision.cameras import * - -# Import the I2C bus -from .bus_i2c import i2c +# Import the Red Vision package. +import red_vision as rv ################################################################################ -# HM01B0 +# DVP Camera ################################################################################ -# PIO interface, only available on Raspberry Pi RP2 processors -camera = hm01b0_pio.HM01B0_PIO( - i2c, - pin_d0 = 12, - pin_vsync = 13, - pin_hsync = 14, - pin_pclk = 15, +############# +# Interface # +############# + +# Import the I2C bus. +from .bus_i2c import i2c + +# PIO interface, only available on Raspberry Pi RP2 processors. +interface = rv.cameras.DVP_RP2_PIO( sm_id = 5, - pin_xclk = None, # Optional xclock pin, specify if needed - num_data_pins = 1 # Number of data pins used by the camera (1, 4, or 8) + pin_d0 = 28, + pin_vsync = 42, + pin_hsync = 41, + pin_pclk = 40, + + # Optionally specify the XCLK pin if needed by your camera. + pin_xclk = 44, ) -################################################################################ -# OV5640 -################################################################################ +########## +# Driver # +########## + +# HM01B0 camera. +driver = rv.cameras.HM01B0( + interface, + i2c, + + # Optionally specify the number of data pins for the camera to use. + # num_data_pins = 1, # Number of data pins used by the camera (1, 4, or 8) -# PIO interface, only available on Raspberry Pi RP2 processors -# camera = ov5640_pio.OV5640_PIO( + # Optionally run in continuous capture mode. + # continuous = False, + + # Optionally specify the image resolution. + # height = 244, + # width = 324, + + # Optionally specify the image buffer to use. + # buffer = None, +) + +# OV5640 camera. +# driver = rv.cameras.OV5640( +# interface, # i2c, -# sm_id = 5, -# pin_d0 = 8, -# pin_vsync = 22, -# pin_hsync = 21, -# pin_pclk = 20, -# pin_xclk = 3 # Optional xclock pin, specify if needed + +# # Optionally run in continuous capture mode. +# # continuous = False, + +# # Optionally specify the image resolution. +# # height = 240, +# # width = 320, + +# # Optionally specify the image color mode. +# # color_mode = rv.colors.COLOR_MODE_BGR565, + +# # Optionally specify the image buffer to use. +# # buffer = None, # ) + +################################################################################ +# Display Object +################################################################################ + +# Here we create the main VideoCapture object using the selected driver. +camera = rv.cameras.VideoCapture(driver) diff --git a/red_vision_examples/rv_init/display.py b/red_vision_examples/rv_init/display.py index 5ff6f1e..727b234 100644 --- a/red_vision_examples/rv_init/display.py +++ b/red_vision_examples/rv_init/display.py @@ -1,58 +1,81 @@ -# Initializes a display object. Multiple options are provided below, so you can -# choose one that best fits your needs. You may need to adjust the arguments -# based on your specific display and board configuration +#------------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 SparkFun Electronics +#------------------------------------------------------------------------------- +# red_vision_examples/rv_init/display.py +# +# This example module initializes a Red Vision display object. Multiple drivers +# and interfaces are provided for various devices, so you can uncomment whatever +# best fits your needs. You may need to adjust the arguments based on your +# specific display and board configuration. The actual display object is created +# at the end of the file. +#------------------------------------------------------------------------------- -# Import the OpenCV display drivers -from red_vision.displays import * - -# Import the SPI bus -from .bus_spi import spi +# Import the Red Vision package. +import red_vision as rv ################################################################################ -# ST7789 +# SPI Display ################################################################################ -# SPI interface. This should work on any platform, but it's not always the -# fastest option (24Mbps on RP2350) -display = st7789_spi.ST7789_SPI( - width = 240, - height = 320, +############# +# Interface # +############# + +# Import the SPI bus +from .bus_spi import spi + +# Generic SPI interface. This should work on any platform, but it's not always +# the fastest option (24Mbps on RP2350). +interface = rv.displays.SPI_Generic( spi = spi, - pin_dc = 16, - pin_cs = 17, - rotation = 1 + pin_dc = 20, + pin_cs = 21, ) # PIO interface. This is only available on Raspberry Pi RP2 processors, -# but is much faster than the SPI interface (75Mbps on RP2350) -# display = st7789_pio.ST7789_PIO( -# width = 240, -# height = 320, +# but is much faster than the SPI interface (75Mbps on RP2350). +# interface = rv.displays.SPI_RP2_PIO( # sm_id = 4, -# pin_clk = 18, -# pin_tx = 19, -# pin_dc = 16, -# pin_cs = 17, -# rotation = 1 +# pin_clk = 22, +# pin_tx = 23, +# pin_dc = 20, +# pin_cs = 21, # ) +########## +# Driver # +########## + +# ST7789 display. +driver = rv.displays.ST7789( + interface = interface, + + # Optionally specify the rotation of the display. + # rotation = 1, + + # Optionally specify the image resolution. + # height = 240, + # width = 320, + + # Optionally specify the image color mode. + # color_mode = rv.colors.COLOR_MODE_BGR565, + + # Optionally specify the image buffer to use. + # buffer = None, +) + ################################################################################ -# DVI +# DVI/HDMI Display ################################################################################ -# HSTX interface. This is only available on Raspberry Pi RP2350 processors. -# Create the singleston DVI_HSTX display instance. -# display = dvi_rp2_hstx.DVI_HSTX( -# width = 320, -# height = 240, - -# # 4 different color modes are supported, though not all color modes can be -# # used with all resolutions due to memory constraints. -# # color_mode = dvi_rp2_hstx.DVI_HSTX.COLOR_BGR233, -# # color_mode = dvi_rp2_hstx.DVI_HSTX.COLOR_GRAY8, -# color_mode = dvi_rp2_hstx.DVI_HSTX.COLOR_BGR565, -# # color_mode = dvi_rp2_hstx.DVI_HSTX.COLOR_BGRA8888, +############# +# Interface # +############# +# HSTX interface. This is only available on Raspberry Pi RP2350 processors. +# interface = rv.displays.DVI_RP2_HSTX( # # Pins default to the SparkFun HSTX to DVI Breakout: # # https://www.sparkfun.com/sparkfun-hstx-to-dvi-breakout.html # # pin_clk_p = 14, @@ -62,5 +85,34 @@ # # pin_d1_p = 16, # # pin_d1_n = 17, # # pin_d2_p = 12, -# # pin_d2_n = 13 +# # pin_d2_n = 13, # ) + +########## +# Driver # +########## + +# DVI/HDMI display. +# driver = rv.displays.DVI( +# interface = interface, + +# # Optionally specify the image resolution. +# # height = 240, +# # width = 320, + +# # Optionally specify the image color mode. +# # color_mode = rv.colors.COLOR_MODE_BGR233, +# # color_mode = rv.colors.COLOR_MODE_GRAY8, +# # color_mode = rv.colors.COLOR_MODE_BGR565, +# # color_mode = rv.colors.COLOR_MODE_BGRA8888, + +# # Optionally specify the image buffer to use. +# # buffer = None, +# ) + +################################################################################ +# Display Object +################################################################################ + +# Here we create the main VideoDisplay object using the selected driver. +display = rv.displays.VideoDisplay(driver) diff --git a/red_vision_examples/rv_init/sd_card.py b/red_vision_examples/rv_init/sd_card.py index 73d3b99..3311bae 100644 --- a/red_vision_examples/rv_init/sd_card.py +++ b/red_vision_examples/rv_init/sd_card.py @@ -1,6 +1,15 @@ -# Initializes SD card and mounts it to the filesystem. This assumes the SD card -# is on the same SPI bus as the display with a different chip select pin. You -# may need to adjust this based on your specific board and configuration +#------------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 SparkFun Electronics +#------------------------------------------------------------------------------- +# red_vision_examples/rv_init/sd_card.py +# +# This example module initializes an SD card and mounts it to the filesystem. +# This assumes the SD card is on the same SPI bus as the display (if applicable) +# with a different chip select pin. You may need to adjust this based on your +# specific board and configuration. +#------------------------------------------------------------------------------- # Import the Pin class for the chip select pin from machine import Pin diff --git a/red_vision_examples/rv_init/touch_screen.py b/red_vision_examples/rv_init/touch_screen.py index f4aa290..f333e9c 100644 --- a/red_vision_examples/rv_init/touch_screen.py +++ b/red_vision_examples/rv_init/touch_screen.py @@ -1,16 +1,22 @@ -# Initializes a touch screen object. Multiple options are provided below, so you -# can choose one that best fits your needs. You may need to adjust the arguments -# based on your specific touch screen and board configuration +#------------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 SparkFun Electronics +#------------------------------------------------------------------------------- +# red_vision_examples/rv_init/touch_screen.py +# +# This example module initializes a Red Vision touch screen object. +#------------------------------------------------------------------------------- -# Import the OpenCV touch screen drivers -from red_vision.touch_screens import * - -# Import the I2C bus -from .bus_i2c import i2c +# Import the Red Vision package. +import red_vision as rv ################################################################################ # CST816 ################################################################################ +# Import the I2C bus +from .bus_i2c import i2c + # I2C interface -touch_screen = cst816.CST816(i2c) +touch_screen = rv.touch_screens.CST816(i2c) diff --git a/red_vision_examples/xrp_examples/ex01_touch_screen_drive.py b/red_vision_examples/xrp_examples/ex01_touch_screen_drive.py index 79730fc..a214c8a 100644 --- a/red_vision_examples/xrp_examples/ex01_touch_screen_drive.py +++ b/red_vision_examples/xrp_examples/ex01_touch_screen_drive.py @@ -3,7 +3,7 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# ex01_touch_screen_drive.py +# red_vision_examples/xrp_examples/ex01_touch_screen_drive.py # # This example creates a simple touch screen interface to drive the XRP robot. # It creates arrow buttons to drive around, and a stop button to exit the diff --git a/red_vision_examples/xrp_examples/ex02_grab_orange_ring.py b/red_vision_examples/xrp_examples/ex02_grab_orange_ring.py index fe0382f..0210ab2 100644 --- a/red_vision_examples/xrp_examples/ex02_grab_orange_ring.py +++ b/red_vision_examples/xrp_examples/ex02_grab_orange_ring.py @@ -3,7 +3,7 @@ # # Copyright (c) 2025 SparkFun Electronics #------------------------------------------------------------------------------- -# ex02_grab_orange_ring.py +# red_vision_examples/xrp_examples/ex02_grab_orange_ring.py # # The XRP can act as a bridge to FIRST programs, which includes summer camps # with FIRST-style games. Learn more here: From 8ea03ee471e2547b9edd56c6bb50313e11db8612 Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Mon, 8 Dec 2025 12:25:43 -0700 Subject: [PATCH 02/11] dvp_rp2_pio: Transfer 4 bytes at a time when there's only 1 byte per pixel. Fixes a regression with the HM01B0 driver, where the DMA is unable to transfer fast enough to keep up with the PIO when only 1 byte per transfer is performed. It's also just more efficient in general. --- red_vision/cameras/dvp_rp2_pio.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/red_vision/cameras/dvp_rp2_pio.py b/red_vision/cameras/dvp_rp2_pio.py index 5975ddc..fbcf613 100644 --- a/red_vision/cameras/dvp_rp2_pio.py +++ b/red_vision/cameras/dvp_rp2_pio.py @@ -83,9 +83,20 @@ def begin( self._xclk.freq(xclk_freq) self._xclk.duty_u16(32768) # 50% duty cycle + # If there's only 1 byte per pixel, we can safely transfer multiple + # pixels at a time without worrying about byte alignment. So we use the + # maximum of 4 pixels per transfer to improve DMA efficiency. + if self._bytes_per_pixel == 1: + self._bytes_per_transfer = 4 + # The PIO left shifts the pixel data in the FIFO buffer, so we need + # to swap the bytes to get the correct order. + byte_swap = True + else: + self._bytes_per_transfer = self._bytes_per_pixel + # Store transfer parameters self._byte_swap = byte_swap - + # Whether to continuously capture frames self._continuous = continuous @@ -121,7 +132,7 @@ def _setup_pio(self): self._sm_id, program, in_base = self._pin_d0, - push_thresh = self._bytes_per_pixel * 8 + push_thresh = self._bytes_per_transfer * 8 ) # Here is the PIO program, which is configurable to mask in the GPIO pins @@ -301,7 +312,7 @@ def _create_dma_ctrl_registers(self): # needed. Once done, it chains back to the dispatcher to get the next # control block. self._dma_ctrl_pio_repeat = self._dma_executer.pack_ctrl( - size = {1:0, 2:1, 4:2}[self._bytes_per_pixel], + size = {1:0, 2:1, 4:2}[self._bytes_per_transfer], inc_read = False, inc_write = True, # ring_size = 0, @@ -427,7 +438,7 @@ def _create_control_blocks(self): self._cb_pio_repeat = array.array('I', [ pio_rx_fifo_addr, # READ_ADDR addressof(self._row_buffer), # WRITE_ADDR - self._bytes_per_row // self._bytes_per_pixel, # TRANS_COUNT + self._bytes_per_row // self._bytes_per_transfer, # TRANS_COUNT self._dma_ctrl_pio_repeat, # CTRL_TRIG ]) From 6d667652821eff623d407e3a853daa677fda37e1 Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Mon, 8 Dec 2025 14:37:48 -0700 Subject: [PATCH 03/11] Increase HM01B0 pin drive strength when using 1-bit mode On RedBoard RP2350, the D0 pin ends up connecting to the HSTX connector. When an HDMI cable is plugged in, D0 connects to the CEC pin, which has the effect of dramatically increasing the capacitance on the D0 pin, resulting in a terrible eye pattern. Increasing the drive strength helps clean it up a lot. Also probably beneficial to do this to the PCLK pin, and on all boards when using 1-bit mode (highest PCLK frequency, same as MCLK pin) to help maintain good signal quality. --- red_vision/cameras/hm01b0.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/red_vision/cameras/hm01b0.py b/red_vision/cameras/hm01b0.py index c99a493..4153a2a 100644 --- a/red_vision/cameras/hm01b0.py +++ b/red_vision/cameras/hm01b0.py @@ -238,6 +238,7 @@ def __init__( i2c, i2c_address = 0x24, num_data_pins = 1, + xclk_freq = 25_000_000, continuous = False, height = None, width = None, @@ -262,7 +263,7 @@ def __init__( self._interface.begin( self._buffer, - xclk_freq = 25_000_000, + xclk_freq = xclk_freq, num_data_pins = self._num_data_pins, byte_swap = False, continuous = self._continuous, @@ -416,3 +417,27 @@ def _send_init(self, num_data_pins): value = 0x02 self._write_register(reg, value) sleep_us(1000) + + # When using only 1 data pin, the HM01B0 sets the PCLK to the same + # frequency as the XCLK, which is typically 24MHz. Because of the high + # frequency, the signal integrity can be more easily compromised. This + # is especially true with the SparkFun IoT RedBoard - RP2350, where the + # D0 pin also goes to the HSTX connector; if an HDMI cable is plugged + # in, it adds a lot of capacitance to the D0 pin. To help with signal + # integrity, we can increase the pin drive strength when using 1 data + # pin. (When 8 data pins are used, PCLK is typically 8x lower frequency + # than XCLK, so signal integrity is much less of a concern.) + if num_data_pins == 1: + # Page 42 of the HM01B0 datasheet: + # https://www.uctronics.com/download/Datasheet/HM01B0-MWA-image-sensor-datasheet.pdf + # 0x3062 (IO_DRIVE_STR): IO drive strength control + # [3:0] : PCLKO + # [7:4] : D[0] + # + # Testing with a RedBoard - RP2350 has shown that setting the D[0] + # drive strength to 3 is sufficient to result in a clean eye pattern + # on an oscilloscope when an HDMI cable is connected. It's also + # beneficial to increase the PCLKO drive strength to create a more + # square wave. Both can be increased to 0xF for a cleaner signal, + # but this increases power consumption and likely creates more EMI. + self._write_register(self._IO_DRIVE_STR, 0x33) From a6a06b96f846ba536a490de783abd228364d117a Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Mon, 8 Dec 2025 17:29:12 -0700 Subject: [PATCH 04/11] Reduce HM01B0 PCLK drive strength Too high seems to maybe cause oscillations or something. Hard to know for certain, because a scope probe changes the behavior. --- red_vision/cameras/hm01b0.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/red_vision/cameras/hm01b0.py b/red_vision/cameras/hm01b0.py index 4153a2a..43f3a38 100644 --- a/red_vision/cameras/hm01b0.py +++ b/red_vision/cameras/hm01b0.py @@ -436,8 +436,14 @@ def _send_init(self, num_data_pins): # # Testing with a RedBoard - RP2350 has shown that setting the D[0] # drive strength to 3 is sufficient to result in a clean eye pattern - # on an oscilloscope when an HDMI cable is connected. It's also - # beneficial to increase the PCLKO drive strength to create a more - # square wave. Both can be increased to 0xF for a cleaner signal, - # but this increases power consumption and likely creates more EMI. - self._write_register(self._IO_DRIVE_STR, 0x33) + # on an oscilloscope when an HDMI cable is connected. Increasing the + # PCLKO drive strength also make a cleaner square wave according to + # the oscilloscope. However, when the scope probe is disconnected, + # the image can become corrupted. The root problem is not known, but + # it's possible that the increased drive strength is causing + # oscillations on the PCLK line when there is no load from the scope + # probe. Additionally, increasing the drive strength increases + # power consumption and likely creates more EMI, so a balance seems + # to be needed. More testing may be needed to find optimal values, + # or make these configurable by the user. + self._write_register(self._IO_DRIVE_STR, 0x30) From 9752efb192af467d39041b813167fa0040cee776 Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Tue, 9 Dec 2025 16:02:48 -0700 Subject: [PATCH 05/11] Reduce OV5640 drive strength Similar to the HM01B0, too high drive strength seems to result in degraded signal integrity, which is not visible with an oscilloscope due to the additional capacitance on the line. The default drive strength was set to the max (4x), which was causing bytes to sometimes be missed with the OV5640. Reducing to 2x seems to be much more stable now, and still sufficiently strong to get a clean eye pattern on the D0 pin while an HDMI cable is connected with the RP2350 RedBoard. --- red_vision/cameras/ov5640.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/red_vision/cameras/ov5640.py b/red_vision/cameras/ov5640.py index f0c79a4..4820316 100644 --- a/red_vision/cameras/ov5640.py +++ b/red_vision/cameras/ov5640.py @@ -417,7 +417,7 @@ class OV5640(DVP_Camera): # io direction 0x3017, 0xFF, 0x3018, 0xFF, - _DRIVE_CAPABILITY, 0xC3, + _DRIVE_CAPABILITY, 0x43, # 2x drive strength _CLOCK_POL_CONTROL, 0x21, 0x4713, 0x02, # jpg mode select _ISP_CONTROL_01, 0x83, # turn color matrix, awb and SDE From 217b47330c415a55829bf078432536cf37918a88 Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Tue, 9 Dec 2025 16:56:00 -0700 Subject: [PATCH 06/11] Further reduce OV5640 drive strength Further testing revealed 2x was still too much. 1x seems much more stable. Also fix erroneous extra bit getting set (not documented in datasheet, so probably shouldn't be changing it). --- red_vision/cameras/ov5640.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/red_vision/cameras/ov5640.py b/red_vision/cameras/ov5640.py index 4820316..8b65713 100644 --- a/red_vision/cameras/ov5640.py +++ b/red_vision/cameras/ov5640.py @@ -417,7 +417,7 @@ class OV5640(DVP_Camera): # io direction 0x3017, 0xFF, 0x3018, 0xFF, - _DRIVE_CAPABILITY, 0x43, # 2x drive strength + _DRIVE_CAPABILITY, 0x02, # 1x drive strength _CLOCK_POL_CONTROL, 0x21, 0x4713, 0x02, # jpg mode select _ISP_CONTROL_01, 0x83, # turn color matrix, awb and SDE From 12bf2b9e9446097734563b574bee106c4f7dbf97 Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Tue, 9 Dec 2025 17:24:41 -0700 Subject: [PATCH 07/11] Add missing arguments to base video driver class --- red_vision/utils/video_driver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/red_vision/utils/video_driver.py b/red_vision/utils/video_driver.py index 30bfaf9..bfe654a 100644 --- a/red_vision/utils/video_driver.py +++ b/red_vision/utils/video_driver.py @@ -78,7 +78,7 @@ def color_mode(self): """ return self._color_mode - def resolution_default(): + def resolution_default(self): """ Returns the default resolution of the camera. @@ -87,7 +87,7 @@ def resolution_default(): """ raise NotImplementedError("Subclass must implement this method") - def resolution_is_supported(): + def resolution_is_supported(self, height, width): """ Returns whether the given resolution is supported by the camera. @@ -99,7 +99,7 @@ def resolution_is_supported(): """ raise NotImplementedError("Subclass must implement this method") - def color_mode_default(): + def color_mode_default(self): """ Returns the default color mode of the camera. @@ -108,7 +108,7 @@ def color_mode_default(): """ raise NotImplementedError("Subclass must implement this method") - def color_mode_is_supported(): + def color_mode_is_supported(self, color_mode): """ Returns the default resolution of the camera. From 59b64975013e1f3f036acba1a037be2ca91e9986 Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Fri, 12 Dec 2025 15:52:05 -0700 Subject: [PATCH 08/11] Add constants for ST7789 rotations --- red_vision/displays/st7789.py | 8 +++++++- red_vision_examples/rv_init/display.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/red_vision/displays/st7789.py b/red_vision/displays/st7789.py index aed4f21..f8dbe49 100644 --- a/red_vision/displays/st7789.py +++ b/red_vision/displays/st7789.py @@ -54,6 +54,12 @@ class ST7789(VideoDisplayDriver): _ENCODE_POS = ">HH" + # Rotation indices + ROTATION_PORTRAIT = 0 + ROTATION_LANDSCAPE = 1 + ROTATION_PORTRAIT_INVERTED = 2 + ROTATION_LANDSCAPE_INVERTED = 3 + # Rotation tables # (madctl, width, height, xstart, ystart)[rotation % 4] @@ -125,7 +131,7 @@ def __init__( width = None, color_mode = None, buffer = None, - rotation = 1, + rotation = ROTATION_LANDSCAPE, ): """ Initializes the ST7789 display driver. diff --git a/red_vision_examples/rv_init/display.py b/red_vision_examples/rv_init/display.py index 727b234..1580597 100644 --- a/red_vision_examples/rv_init/display.py +++ b/red_vision_examples/rv_init/display.py @@ -53,7 +53,7 @@ interface = interface, # Optionally specify the rotation of the display. - # rotation = 1, + # rotation = rv.displays.ST7789.ROTATION_LANDSCAPE, # Optionally specify the image resolution. # height = 240, From 982f3ea9ec2eaa249d7fc91c0fd9a6c25ddc46bc Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Fri, 12 Dec 2025 16:09:04 -0700 Subject: [PATCH 09/11] Import memory and color modules with 'rv_' prefix --- red_vision/cameras/dvp_rp2_pio.py | 6 +++--- red_vision/cameras/hm01b0.py | 6 +++--- red_vision/cameras/ov5640.py | 6 +++--- red_vision/cameras/video_capture.py | 20 ++++++++++---------- red_vision/displays/dvi_rp2_hstx.py | 24 ++++++++++++------------ red_vision/displays/st7789.py | 6 +++--- red_vision/displays/video_display.py | 20 ++++++++++---------- red_vision/utils/video_driver.py | 4 ++-- 8 files changed, 46 insertions(+), 46 deletions(-) diff --git a/red_vision/cameras/dvp_rp2_pio.py b/red_vision/cameras/dvp_rp2_pio.py index fbcf613..323720e 100644 --- a/red_vision/cameras/dvp_rp2_pio.py +++ b/red_vision/cameras/dvp_rp2_pio.py @@ -18,7 +18,7 @@ import array from machine import Pin, PWM from uctypes import addressof -from ..utils import memory +from ..utils import memory as rv_memory class DVP_RP2_PIO(): """ @@ -247,7 +247,7 @@ def _setup_dmas(self): self._dma_executer = rp2.DMA() # Check if the display buffer is in PSRAM. - self._buffer_is_in_psram = memory.is_in_external_ram(self._buffer) + self._buffer_is_in_psram = rv_memory.is_in_external_ram(self._buffer) # If the buffer is in PSRAM, create the streamer DMA channel and row # buffer in SRAM. @@ -261,7 +261,7 @@ def _setup_dmas(self): # Verify row buffer is in SRAM. If not, we'll still have the same # latency problem. - if memory.is_in_external_ram(self._row_buffer): + if rv_memory.is_in_external_ram(self._row_buffer): raise MemoryError("not enough space in SRAM for row buffer") # Create DMA control register values. diff --git a/red_vision/cameras/hm01b0.py b/red_vision/cameras/hm01b0.py index 43f3a38..330f4b2 100644 --- a/red_vision/cameras/hm01b0.py +++ b/red_vision/cameras/hm01b0.py @@ -15,7 +15,7 @@ from .dvp_camera import DVP_Camera from time import sleep_us -from ..utils import colors +from ..utils import colors as rv_colors class HM01B0(DVP_Camera): """ @@ -300,7 +300,7 @@ def color_mode_default(self): Returns: int: Color mode constant """ - return colors.COLOR_MODE_BAYER_RG + return rv_colors.COLOR_MODE_BAYER_RG def color_mode_is_supported(self, color_mode): """ @@ -311,7 +311,7 @@ def color_mode_is_supported(self, color_mode): Returns: bool: True if the color mode is supported, otherwise False """ - return color_mode == colors.COLOR_MODE_BAYER_RG + return color_mode == rv_colors.COLOR_MODE_BAYER_RG def open(self): """ diff --git a/red_vision/cameras/ov5640.py b/red_vision/cameras/ov5640.py index 8b65713..241b528 100644 --- a/red_vision/cameras/ov5640.py +++ b/red_vision/cameras/ov5640.py @@ -15,7 +15,7 @@ from .dvp_camera import DVP_Camera from time import sleep_us -from ..utils import colors +from ..utils import colors as rv_colors class OV5640(DVP_Camera): """ @@ -960,7 +960,7 @@ def color_mode_default(self): Returns: int: Color mode constant """ - return colors.COLOR_MODE_BGR565 + return rv_colors.COLOR_MODE_BGR565 def color_mode_is_supported(self, color_mode): """ @@ -971,7 +971,7 @@ def color_mode_is_supported(self, color_mode): Returns: bool: True if the color mode is supported, otherwise False """ - return color_mode == colors.COLOR_MODE_BGR565 + return color_mode == rv_colors.COLOR_MODE_BGR565 def open(self): """ diff --git a/red_vision/cameras/video_capture.py b/red_vision/cameras/video_capture.py index 75653d5..fdc8573 100644 --- a/red_vision/cameras/video_capture.py +++ b/red_vision/cameras/video_capture.py @@ -10,7 +10,7 @@ #------------------------------------------------------------------------------- import cv2 -from ..utils import colors +from ..utils import colors as rv_colors class VideoCapture(): """ @@ -61,9 +61,9 @@ def retrieve(self, image = None): """ color_mode = self._driver.color_mode() buffer = self._driver.buffer() - if (color_mode == colors.COLOR_MODE_BGR888 or - color_mode == colors.COLOR_MODE_GRAY8 or - color_mode == colors.COLOR_MODE_BGR233): # No conversion available + if (color_mode == rv_colors.COLOR_MODE_BGR888 or + color_mode == rv_colors.COLOR_MODE_GRAY8 or + color_mode == rv_colors.COLOR_MODE_BGR233): # No conversion available # These color modes are copied directly with no conversion. if image is not None: # Copy buffer to provided image. @@ -72,17 +72,17 @@ def retrieve(self, image = None): else: # Return a copy of the buffer. return (True, buffer.copy()) - elif color_mode == colors.COLOR_MODE_BAYER_BG: + elif color_mode == rv_colors.COLOR_MODE_BAYER_BG: return (True, cv2.cvtColor(buffer, cv2.COLOR_BayerBG2BGR, image)) - elif color_mode == colors.COLOR_MODE_BAYER_GB: + elif color_mode == rv_colors.COLOR_MODE_BAYER_GB: return (True, cv2.cvtColor(buffer, cv2.COLOR_BayerGB2BGR, image)) - elif color_mode == colors.COLOR_MODE_BAYER_RG: + elif color_mode == rv_colors.COLOR_MODE_BAYER_RG: return (True, cv2.cvtColor(buffer, cv2.COLOR_BayerRG2BGR, image)) - elif color_mode == colors.COLOR_MODE_BAYER_GR: + elif color_mode == rv_colors.COLOR_MODE_BAYER_GR: return (True, cv2.cvtColor(buffer, cv2.COLOR_BayerGR2BGR, image)) - elif color_mode == colors.COLOR_MODE_BGR565: + elif color_mode == rv_colors.COLOR_MODE_BGR565: return (True, cv2.cvtColor(buffer, cv2.COLOR_BGR5652BGR, image)) - elif color_mode == colors.COLOR_MODE_BGRA8888: + elif color_mode == rv_colors.COLOR_MODE_BGRA8888: return (True, cv2.cvtColor(buffer, cv2.COLOR_BGRA2BGR, image)) else: NotImplementedError("Unsupported color mode") diff --git a/red_vision/displays/dvi_rp2_hstx.py b/red_vision/displays/dvi_rp2_hstx.py index 7f3cd5c..1365227 100644 --- a/red_vision/displays/dvi_rp2_hstx.py +++ b/red_vision/displays/dvi_rp2_hstx.py @@ -20,8 +20,8 @@ import array from uctypes import addressof from ulab import numpy as np -from ..utils import colors -from ..utils import memory +from ..utils import colors as rv_colors +from ..utils import memory as rv_memory class DVI_RP2_HSTX(): """ @@ -200,7 +200,7 @@ def color_mode_default(self): """ Returns the default color mode for the display. """ - return colors.COLOR_MODE_BGR565 + return rv_colors.COLOR_MODE_BGR565 def color_mode_is_supported(self, color_mode): """ @@ -211,7 +211,7 @@ def color_mode_is_supported(self, color_mode): Returns: bool: True if the color mode is supported, otherwise False """ - return color_mode == colors.COLOR_MODE_BGR565 + return color_mode == rv_colors.COLOR_MODE_BGR565 def _configure_hstx(self): """ @@ -261,7 +261,7 @@ def _configure_hstx(self): # With BGR color modes, B is the least significant bits, and R is the # most significant bits. This means the bits are in RGB order, which is # opposite of what one might expect. - if self._color_mode == colors.COLOR_MODE_BGR233: + if self._color_mode == rv_colors.COLOR_MODE_BGR233: # BGR233 (00000000 00000000 00000000 RRRGGGBB) expand_tmds = self._hstx.pack_expand_tmds( l2_nbits = 2, # 3 bits (red) @@ -271,7 +271,7 @@ def _configure_hstx(self): l0_nbits = 1, # 2 bits (blue) l0_rot = 26, # Shift right 26 bits to align MSB (left 6 bits) ) - elif self._color_mode == colors.COLOR_MODE_GRAY8: + elif self._color_mode == rv_colors.COLOR_MODE_GRAY8: # GRAY8 (00000000 00000000 00000000 GGGGGGGG) expand_tmds = self._hstx.pack_expand_tmds( l2_nbits = 7, # 8 bits (red) @@ -281,7 +281,7 @@ def _configure_hstx(self): l0_nbits = 7, # 8 bits (blue) l0_rot = 0, # Shift right 0 bits to align MSB ) - elif self._color_mode == colors.COLOR_MODE_BGR565: + elif self._color_mode == rv_colors.COLOR_MODE_BGR565: # BGR565 (00000000 00000000 RRRRRGGG GGGBBBBB) expand_tmds = self._hstx.pack_expand_tmds( l2_nbits = 4, # 5 bits (red) @@ -291,7 +291,7 @@ def _configure_hstx(self): l0_nbits = 4, # 5 bits (blue) l0_rot = 29, # Shift right 29 bits to align MSB (left 3 bits) ) - elif self._color_mode == colors.COLOR_MODE_BGRA8888: + elif self._color_mode == rv_colors.COLOR_MODE_BGRA8888: # BGRA8888 (AAAAAAAA RRRRRRRR GGGGGGGG BBBBBBBB) alpha is ignored expand_tmds = self._hstx.pack_expand_tmds( l2_nbits = 7, # 8 bits (red) @@ -526,7 +526,7 @@ def _configure_dmas(self): self._dma_executer = rp2.DMA() # Check if the display buffer is in PSRAM. - self._buffer_is_in_psram = memory.is_in_external_ram(self._buffer) + self._buffer_is_in_psram = rv_memory.is_in_external_ram(self._buffer) # If the buffer is in PSRAM, create the streamer DMA channel and row # buffer in SRAM. @@ -544,7 +544,7 @@ def _configure_dmas(self): # BGR233 or GRAY8). Larger color modes (2 or 4 bytes per pixel) can # only be used with scaling. hstx_pixels_per_second = machine.freq() / 5 - psram_bytes_per_second = memory.external_ram_max_bytes_per_second() + psram_bytes_per_second = rv_memory.external_ram_max_bytes_per_second() psram_pixels_per_second = psram_bytes_per_second * self._width_scale / self._bytes_per_pixel if psram_pixels_per_second < hstx_pixels_per_second: raise ValueError("PSRAM transfer speed too low for specified resolution and color mode") @@ -555,7 +555,7 @@ def _configure_dmas(self): # Verify row buffer is in SRAM. If not, we'll still have the same # latency problem. - if memory.is_in_external_ram(self._row_buffer): + if rv_memory.is_in_external_ram(self._row_buffer): raise MemoryError("not enough space in SRAM for row buffer") # We'll use a DMA to trigger the XIP stream. However the RP2350's @@ -725,7 +725,7 @@ def _create_control_blocks(self): # The control block array must be in SRAM, otherwise we run into the # same latency problem with DMA transfers from PSRAM. - if memory.is_in_external_ram(self._control_blocks): + if rv_memory.is_in_external_ram(self._control_blocks): raise MemoryError("not enough space in SRAM for control block array") # Create the HSTX command sequences so the control blocks can reference diff --git a/red_vision/displays/st7789.py b/red_vision/displays/st7789.py index f8dbe49..217e19b 100644 --- a/red_vision/displays/st7789.py +++ b/red_vision/displays/st7789.py @@ -18,7 +18,7 @@ from time import sleep_ms import struct -from ..utils import colors +from ..utils import colors as rv_colors from .video_display_driver import VideoDisplayDriver class ST7789(VideoDisplayDriver): @@ -208,7 +208,7 @@ def color_mode_default(self): """ Returns the default color mode for the display. """ - return colors.COLOR_MODE_BGR565 + return rv_colors.COLOR_MODE_BGR565 def color_mode_is_supported(self, color_mode): """ @@ -219,7 +219,7 @@ def color_mode_is_supported(self, color_mode): Returns: bool: True if the color mode is supported, otherwise False """ - return color_mode == colors.COLOR_MODE_BGR565 + return color_mode == rv_colors.COLOR_MODE_BGR565 def show(self): """ diff --git a/red_vision/displays/video_display.py b/red_vision/displays/video_display.py index 5cac326..2f412f7 100644 --- a/red_vision/displays/video_display.py +++ b/red_vision/displays/video_display.py @@ -11,7 +11,7 @@ import cv2 as cv from ulab import numpy as np -from ..utils import colors +from ..utils import colors as rv_colors class VideoDisplay(): """ @@ -46,19 +46,19 @@ def imshow(self, image): # Convert the image to current format and write it to the buffer. color_mode = self._driver.color_mode() - if (color_mode == colors.COLOR_MODE_GRAY8 or + if (color_mode == rv_colors.COLOR_MODE_GRAY8 or # No conversion available for the modes below, treat as GRAY8 - color_mode == colors.COLOR_MODE_BAYER_BG or - color_mode == colors.COLOR_MODE_BAYER_GB or - color_mode == colors.COLOR_MODE_BAYER_RG or - color_mode == colors.COLOR_MODE_BAYER_GR or - color_mode == colors.COLOR_MODE_BGR233): + color_mode == rv_colors.COLOR_MODE_BAYER_BG or + color_mode == rv_colors.COLOR_MODE_BAYER_GB or + color_mode == rv_colors.COLOR_MODE_BAYER_RG or + color_mode == rv_colors.COLOR_MODE_BAYER_GR or + color_mode == rv_colors.COLOR_MODE_BGR233): self._convert_to_gray8(image_roi, buffer_roi) - elif color_mode == colors.COLOR_MODE_BGR565: + elif color_mode == rv_colors.COLOR_MODE_BGR565: self._convert_to_bgr565(image_roi, buffer_roi) - elif color_mode == colors.COLOR_MODE_BGR888: + elif color_mode == rv_colors.COLOR_MODE_BGR888: self._convert_to_bgr888(image_roi, buffer_roi) - elif color_mode == colors.COLOR_MODE_BGRA8888: + elif color_mode == rv_colors.COLOR_MODE_BGRA8888: self._convert_to_bgra8888(image_roi, buffer_roi) else: raise ValueError("Unsupported color mode") diff --git a/red_vision/utils/video_driver.py b/red_vision/utils/video_driver.py index bfe654a..921091b 100644 --- a/red_vision/utils/video_driver.py +++ b/red_vision/utils/video_driver.py @@ -9,7 +9,7 @@ #------------------------------------------------------------------------------- from ulab import numpy as np -from . import colors +from . import colors as rv_colors class VideoDriver(): """ @@ -51,7 +51,7 @@ def __init__( self._color_mode = color_mode # Create or store the image buffer. - self._bytes_per_pixel = colors.bytes_per_pixel(self._color_mode) + self._bytes_per_pixel = rv_colors.bytes_per_pixel(self._color_mode) buffer_shape = (self._height, self._width, self._bytes_per_pixel) if buffer is None: # No buffer provided, create a new one. From f5f4f0aea2ac28ed32007527ee4ef445722140ca Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Fri, 12 Dec 2025 17:01:13 -0700 Subject: [PATCH 10/11] Fix docstrings and comments --- red_vision/cameras/dvp_camera.py | 23 +++++++++----- red_vision/cameras/dvp_rp2_pio.py | 33 +++++++++++++++------ red_vision/cameras/hm01b0.py | 22 ++++++++++++-- red_vision/cameras/ov5640.py | 22 ++++++++++++-- red_vision/cameras/video_capture.py | 5 +++- red_vision/cameras/video_capture_driver.py | 2 ++ red_vision/displays/dvi.py | 20 ++++++------- red_vision/displays/dvi_rp2_hstx.py | 12 +++----- red_vision/displays/spi_generic.py | 14 +-------- red_vision/displays/spi_rp2_pio.py | 16 ++-------- red_vision/displays/st7789.py | 23 +++++++------- red_vision/displays/video_display.py | 4 +-- red_vision/displays/video_display_driver.py | 2 ++ red_vision/utils/video_driver.py | 26 ++++++++++------ 14 files changed, 133 insertions(+), 91 deletions(-) diff --git a/red_vision/cameras/dvp_camera.py b/red_vision/cameras/dvp_camera.py index 3a816a6..86a30c2 100644 --- a/red_vision/cameras/dvp_camera.py +++ b/red_vision/cameras/dvp_camera.py @@ -29,19 +29,26 @@ def __init__( Args: i2c (I2C): I2C object for communication i2c_address (int): I2C address of the camera + height (int, optional): Image height in pixels + width (int, optional): Image width in pixels + color_mode (int, optional): Color mode to use + buffer (ndarray, optional): Pre-allocated image buffer """ - super().__init__(height, width, color_mode, buffer) - + # Store I2C parameters. self._i2c = i2c self._i2c_address = i2c_address + # Initialize the base VideoCaptureDriver class + super().__init__(height, width, color_mode, buffer) + def _read_register(self, reg, nbytes=1): """ - Reads a register from the camera over I2C. + Reads a register(s) from the camera over I2C. Args: - reg (int): Register address to read - nbytes (int): Number of bytes to read from the register + reg (int): Start register address to read + nbytes (int, optional): Number of bytes to read from the register + (default: 1) Returns: bytes: Data read from the register @@ -51,11 +58,11 @@ def _read_register(self, reg, nbytes=1): def _write_register(self, reg, data): """ - Writes data to a register on the camera over I2C. + Writes data to a register(s) on the camera over I2C. Args: - reg (int): Register address to write - data (bytes, int, list, tuple): Data to write to the register + reg (int): Start register address to write + data (bytes, int, list, tuple): Data to write to the register(s) """ if isinstance(data, int): data = bytes([data]) diff --git a/red_vision/cameras/dvp_rp2_pio.py b/red_vision/cameras/dvp_rp2_pio.py index 323720e..03194a7 100644 --- a/red_vision/cameras/dvp_rp2_pio.py +++ b/red_vision/cameras/dvp_rp2_pio.py @@ -38,25 +38,22 @@ def __init__( Initializes the DVP interface with the specified parameters. Args: + sm_id (int): PIO state machine ID pin_d0 (int): Data 0 pin number for DVP interface pin_vsync (int): Vertical sync pin number pin_hsync (int): Horizontal sync pin number pin_pclk (int): Pixel clock pin number - pin_xclk (int): External clock pin number - xclk_freq (int): Frequency in Hz for the external clock - sm_id (int): PIO state machine ID - num_data_pins (int): Number of data pins used in DVP interface - bytes_per_pixel (int): Number of bytes per pixel - byte_swap (bool): Whether to swap bytes in the captured data - continuous (bool): Whether to continuously capture frames + pin_xclk (int, optional): External clock pin number """ + # Store state machine ID + self._sm_id = sm_id + # Store pin assignments self._pin_d0 = pin_d0 self._pin_vsync = pin_vsync self._pin_hsync = pin_hsync self._pin_pclk = pin_pclk self._pin_xclk = pin_xclk - self._sm_id = sm_id def begin( self, @@ -66,6 +63,18 @@ def begin( byte_swap, continuous = False, ): + """ + Begins the DVP interface with the specified parameters. + + Args: + buffer (ndarray): Image buffer to write captured frames into + xclk_freq (int): Frequency in Hz for the XCLK pin, if it is used + num_data_pins (int): Number of data pins used by the camera (1 to 8) + byte_swap (bool): Whether to swap bytes in each pixel + continuous (bool, optional): Whether to continuously capture frames + (default: False) + """ + # Store buffer and its dimensions self._buffer = buffer self._height, self._width, self._bytes_per_pixel = buffer.shape @@ -108,7 +117,7 @@ def begin( def buffer(self): """ - Returns the current frame buffer from the camera. + Returns the current frame buffer used by this driver. Returns: ndarray: Frame buffer @@ -116,6 +125,9 @@ def buffer(self): return self._buffer def _setup_pio(self): + """ + Sets up the PIO state machine for the DVP interface. + """ # Copy the PIO program program = self._pio_read_dvp @@ -505,6 +517,9 @@ def _assemble_control_blocks(self): def _add_control_block(self, block): """ Helper function to add a control block to the control block array. + + Args: + block (array): Control block to add """ # Add the control block to the array. Each control block is all 4 DMA # alias 0 registers in order. diff --git a/red_vision/cameras/hm01b0.py b/red_vision/cameras/hm01b0.py index 330f4b2..8fd12be 100644 --- a/red_vision/cameras/hm01b0.py +++ b/red_vision/cameras/hm01b0.py @@ -249,18 +249,33 @@ def __init__( Initializes the HM01B0 camera with default settings. Args: + interface (DVP_Interface): DVP interface driver i2c (I2C): I2C object for communication - i2c_address (int, optional): I2C address (default: 0x24) - num_data_pins (int, optional): Number of data pins - - 1 (Default) + i2c_address (int, optional): I2C address of the camera (default: 0x24) + num_data_pins (int, optional): Number of data pins for the camera to + use: + - 1 (default) - 4 - 8 + xclk_freq (int, optional): Frequency of the XCLK signal in Hz + (default: 25MHz) + continuous (bool, optional): Whether to run in continuous capture + mode (default: False) + height (int, optional): Image height in pixels + width (int, optional): Image width in pixels + color_mode (int, optional): Color mode to use: + - COLOR_MODE_BAYER_RG (default) + buffer (ndarray, optional): Pre-allocated image buffer """ + # Store parameters self._interface = interface self._continuous = continuous self._num_data_pins = num_data_pins + + # Initialize the base DVP_Camera class super().__init__(i2c, i2c_address, height, width, color_mode, buffer) + # Begin the interface driver self._interface.begin( self._buffer, xclk_freq = xclk_freq, @@ -269,6 +284,7 @@ def __init__( continuous = self._continuous, ) + # Reset and initialize the camera self._soft_reset() self._send_init(self._num_data_pins) diff --git a/red_vision/cameras/ov5640.py b/red_vision/cameras/ov5640.py index 241b528..4e1138b 100644 --- a/red_vision/cameras/ov5640.py +++ b/red_vision/cameras/ov5640.py @@ -891,6 +891,7 @@ def __init__( interface, i2c, i2c_address = 0x3C, + xclk_freq = 20_000_000, continuous = False, height = None, width = None, @@ -901,23 +902,39 @@ def __init__( Initializes the OV5640 camera sensor with default settings. Args: + interface (DVP_Interface): DVP interface driver i2c (I2C): I2C object for communication - i2c_address (int, optional): I2C address (default: 0x3C) + i2c_address (int, optional): I2C address of the camera (default: 0x3C) + xclk_freq (int, optional): Frequency of the XCLK signal in Hz + (default: 20MHz) + continuous (bool, optional): Whether to run in continuous capture + mode (default: False) + height (int, optional): Image height in pixels + width (int, optional): Image width in pixels + color_mode (int, optional): Color mode to use: + - COLOR_MODE_BGR565 (default) + buffer (ndarray, optional): Pre-allocated image buffer """ + # Store parameters self._interface = interface self._continuous = continuous + + # Initialize the base DVP_Camera class super().__init__(i2c, i2c_address, height, width, color_mode, buffer) + # Begin the interface driver self._interface.begin( self._buffer, - xclk_freq = 20_000_000, + xclk_freq = xclk_freq, num_data_pins = 8, byte_swap = False, continuous = self._continuous, ) + # Initialize the camera self._write_list(self._sensor_default_regs) + # Set default settings self._colorspace = self._OV5640_COLOR_RGB self._flip_x = True self._flip_y = True @@ -930,6 +947,7 @@ def __init__( self._ev = 0 self._white_balance = 0 + # Apply the initial size and colorspace self._set_size_and_colorspace() def resolution_default(self): diff --git a/red_vision/cameras/video_capture.py b/red_vision/cameras/video_capture.py index fdc8573..0fc3baa 100644 --- a/red_vision/cameras/video_capture.py +++ b/red_vision/cameras/video_capture.py @@ -22,7 +22,10 @@ def __init__( driver, ): """ - Initializes the camera. + Initializes a VideoCapture object with the provided driver. + + Args: + driver (VideoCaptureDriver): Camera driver to use """ # Store driver reference. self._driver = driver diff --git a/red_vision/cameras/video_capture_driver.py b/red_vision/cameras/video_capture_driver.py index 850b1ad..0a05446 100644 --- a/red_vision/cameras/video_capture_driver.py +++ b/red_vision/cameras/video_capture_driver.py @@ -14,6 +14,8 @@ class VideoCaptureDriver(VideoDriver): """ Red Vision abstract base class for camera drivers. """ + # No __init__() here, see VideoDriver. + def open(self): """ Opens the camera and prepares it for capturing images. diff --git a/red_vision/displays/dvi.py b/red_vision/displays/dvi.py index 5c7e4e2..e522072 100644 --- a/red_vision/displays/dvi.py +++ b/red_vision/displays/dvi.py @@ -28,20 +28,20 @@ def __init__( Args: width (int): Display width in pixels height (int): Display height in pixels - rotation (int, optional): Orientation of display - - 0: Portrait (default) - - 1: Landscape - - 2: Inverted portrait - - 3: Inverted landscape - bgr_order (bool, optional): Color order - - True: BGR (default) - - False: RGB - reverse_bytes_in_word (bool, optional): - - Enable if the display uses LSB byte order for color words + color_mode (int, optional): Color mode + - COLOR_BGR233 + - COLOR_GRAY8 + - COLOR_BGR565 (default) + - COLOR_BGRA8888 + buffer (ndarray, optional): Pre-allocated frame buffer. """ + # Store parameters self._interface = interface + + # Initialize the base display class super().__init__(height, width, color_mode, buffer) + # Begin the interface driver self._interface.begin( self._buffer, self._color_mode, diff --git a/red_vision/displays/dvi_rp2_hstx.py b/red_vision/displays/dvi_rp2_hstx.py index 1365227..eace4dd 100644 --- a/red_vision/displays/dvi_rp2_hstx.py +++ b/red_vision/displays/dvi_rp2_hstx.py @@ -120,13 +120,6 @@ def __init__( Initializes the DVI HSTX display driver. Args: - width (int): Display width in pixels - height (int): Display height in pixels - color_mode (int, optional): Color mode - - COLOR_BGR233: 8-bit BGR233 - - COLOR_GRAY8: 8-bit grayscale - - COLOR_BGR565: 16-bit BGR565 (default) - - COLOR_BGRA8888: 32-bit BGRA8888 pin_clk_p (int, optional): TMDS clock lane positive pin (default: 14) pin_clk_n (int, optional): TMDS clock lane negative pin (default: 15) pin_d0_p (int, optional): TMDS data 0 lane positive pin (default: 18) @@ -135,7 +128,6 @@ def __init__( pin_d1_n (int, optional): TMDS data 1 lane negative pin (default: 17) pin_d2_p (int, optional): TMDS data 2 lane positive pin (default: 12) pin_d2_n (int, optional): TMDS data 2 lane negative pin (default: 13) - buffer (ndarray, optional): Pre-allocated frame buffer. """ # Set pin numbers. self._pin_clk_p = pin_clk_p @@ -150,6 +142,10 @@ def __init__( def begin(self, buffer, color_mode): """ Begins DVI output. + + Args: + buffer (ndarray): Image buffer to read pixel data from + color_mode (int): Color mode of the image buffer """ # Store buffer and color mode. self._buffer = buffer diff --git a/red_vision/displays/spi_generic.py b/red_vision/displays/spi_generic.py index 212ff03..afad77e 100644 --- a/red_vision/displays/spi_generic.py +++ b/red_vision/displays/spi_generic.py @@ -33,21 +33,9 @@ def __init__( Initializes the ST7789 SPI display driver. Args: - width (int): Display width in pixels - height (int): Display height in pixels - spi (SPI): SPI bus object + spi (SPI): SPI interface object pin_dc (int): Data/Command pin number pin_cs (int, optional): Chip Select pin number - rotation (int, optional): Orientation of display - - 0: Portrait (default) - - 1: Landscape - - 2: Inverted portrait - - 3: Inverted landscape - bgr_order (bool, optional): Color order - - True: BGR (default) - - False: RGB - reverse_bytes_in_word (bool, optional): - - Enable if the display uses LSB byte order for color words """ # Store SPI arguments self._spi = spi diff --git a/red_vision/displays/spi_rp2_pio.py b/red_vision/displays/spi_rp2_pio.py index ef47179..eaabe08 100644 --- a/red_vision/displays/spi_rp2_pio.py +++ b/red_vision/displays/spi_rp2_pio.py @@ -39,25 +39,13 @@ def __init__( Initializes the ST7789 PIO display driver. Args: - width (int): Display width in pixels - height (int): Display height in pixels sm_id (int): PIO state machine ID pin_clk (int): Clock pin number pin_tx (int): Data pin number pin_dc (int): Data/Command pin number pin_cs (int, optional): Chip Select pin number - freq (int, optional): Frequency in Hz for the PIO state machine - Default is -1, which uses the default frequency of 125MHz - rotation (int, optional): Orientation of display - - 0: Portrait (default) - - 1: Landscape - - 2: Inverted portrait - - 3: Inverted landscape - bgr_order (bool, optional): Color order - - True: BGR (default) - - False: RGB - reverse_bytes_in_word (bool, optional): - - Enable if the display uses LSB byte order for color words + freq (int, optional): Frequency in Hz for the PIO state machine. + Default is -1, which uses the system clock frequency """ # Store PIO arguments self._sm_id = sm_id diff --git a/red_vision/displays/st7789.py b/red_vision/displays/st7789.py index 217e19b..4f4b42f 100644 --- a/red_vision/displays/st7789.py +++ b/red_vision/displays/st7789.py @@ -127,28 +127,27 @@ class ST7789(VideoDisplayDriver): def __init__( self, interface, + rotation = ROTATION_LANDSCAPE, height = None, width = None, color_mode = None, buffer = None, - rotation = ROTATION_LANDSCAPE, ): """ Initializes the ST7789 display driver. Args: - width (int): Display width in pixels - height (int): Display height in pixels + interface (SPI_Interface): Display interface driver rotation (int, optional): Orientation of display - - 0: Portrait (default) - - 1: Landscape - - 2: Inverted portrait - - 3: Inverted landscape - bgr_order (bool, optional): Color order - - True: BGR (default) - - False: RGB - reverse_bytes_in_word (bool, optional): - - Enable if the display uses LSB byte order for color words + - ROTATION_PORTRAIT + - ROTATION_LANDSCAPE (default) + - ROTATION_PORTRAIT_INVERTED + - ROTATION_LANDSCAPE_INVERTED + height (int): Display height in pixels + width (int): Display width in pixels + color_mode (int, optional): Color mode to use: + - COLOR_MODE_BGR565 (default) + buffer (ndarray, optional): Pre-allocated image buffer """ self._interface = interface super().__init__(height, width, color_mode, buffer) diff --git a/red_vision/displays/video_display.py b/red_vision/displays/video_display.py index 2f412f7..df359be 100644 --- a/red_vision/displays/video_display.py +++ b/red_vision/displays/video_display.py @@ -23,10 +23,10 @@ def __init__( driver, ): """ - Initializes the display. + Initializes a VideoDisplay object with the provided driver. Args: - buffer_shape (tuple): Shape of the buffer as (rows, cols, channels) + driver (VideoDisplayDriver): Display driver to use """ # Store driver reference. self._driver = driver diff --git a/red_vision/displays/video_display_driver.py b/red_vision/displays/video_display_driver.py index 28d7845..72429e5 100644 --- a/red_vision/displays/video_display_driver.py +++ b/red_vision/displays/video_display_driver.py @@ -14,6 +14,8 @@ class VideoDisplayDriver(VideoDriver): """ Red Vision abstract base class for display drivers. """ + # No __init__() here, see VideoDriver. + def show(self): """ Updates the display with the contents of the framebuffer. diff --git a/red_vision/utils/video_driver.py b/red_vision/utils/video_driver.py index 921091b..4688fce 100644 --- a/red_vision/utils/video_driver.py +++ b/red_vision/utils/video_driver.py @@ -23,7 +23,13 @@ def __init__( buffer = None, ): """ - Initializes the camera. + Initializes a driver with the specified parameters. + + Args: + height (int, optional): Image height in pixels + width (int, optional): Image width in pixels + color_mode (int, optional): Color mode to use + buffer (ndarray, optional): Pre-allocated image buffer """ # Determine image resolution. if height is None or width is None: @@ -65,22 +71,22 @@ def __init__( def buffer(self): """ - Returns the framebuffer used by the display. + Returns the image buffer used by the driver. Returns: - ndarray: Framebuffer + ndarray: Image buffer """ return self._buffer def color_mode(self): """ - Returns the current color mode of the display. + Returns the current color mode of the driver. """ return self._color_mode def resolution_default(self): """ - Returns the default resolution of the camera. + Returns the default resolution of the driver. Returns: tuple: (height, width) @@ -89,7 +95,7 @@ def resolution_default(self): def resolution_is_supported(self, height, width): """ - Returns whether the given resolution is supported by the camera. + Returns whether the given resolution is supported by the driver. Args: height (int): Image height @@ -101,7 +107,7 @@ def resolution_is_supported(self, height, width): def color_mode_default(self): """ - Returns the default color mode of the camera. + Returns the default color mode of the driver. Returns: int: Color mode @@ -110,9 +116,11 @@ def color_mode_default(self): def color_mode_is_supported(self, color_mode): """ - Returns the default resolution of the camera. + Returns whether the given color mode is supported by the driver. + Args: + color_mode (int): Color mode to check Returns: - tuple: (height, width) + bool: True if supported, False otherwise """ raise NotImplementedError("Subclass must implement this method") From 90079d880f8ce34b87c8ba533f62a33822b16190 Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Fri, 12 Dec 2025 17:19:34 -0700 Subject: [PATCH 11/11] Add __init__() to VideoCaptureDriver and VideoDisplayDriver --- red_vision/cameras/video_capture_driver.py | 6 +++++- red_vision/displays/video_display_driver.py | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/red_vision/cameras/video_capture_driver.py b/red_vision/cameras/video_capture_driver.py index 0a05446..c36724a 100644 --- a/red_vision/cameras/video_capture_driver.py +++ b/red_vision/cameras/video_capture_driver.py @@ -14,7 +14,11 @@ class VideoCaptureDriver(VideoDriver): """ Red Vision abstract base class for camera drivers. """ - # No __init__() here, see VideoDriver. + def __init__(self, *args, **kwargs): + """ + Initializes the camera driver. See VideoDriver for parameters. + """ + super().__init__(*args, **kwargs) def open(self): """ diff --git a/red_vision/displays/video_display_driver.py b/red_vision/displays/video_display_driver.py index 72429e5..8a95858 100644 --- a/red_vision/displays/video_display_driver.py +++ b/red_vision/displays/video_display_driver.py @@ -14,10 +14,14 @@ class VideoDisplayDriver(VideoDriver): """ Red Vision abstract base class for display drivers. """ - # No __init__() here, see VideoDriver. + def __init__(self, *args, **kwargs): + """ + Initializes the display driver. See VideoDriver for parameters. + """ + super().__init__(*args, **kwargs) def show(self): """ - Updates the display with the contents of the framebuffer. + Updates the display with the contents of the image buffer. """ raise NotImplementedError("Subclass must implement this method")