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..fbcf613 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,65 @@ 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 + # 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._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 @@ -108,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 @@ -128,22 +152,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 +247,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 +261,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. @@ -304,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, @@ -430,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 ]) diff --git a/red_vision/cameras/hm01b0.py b/red_vision/cameras/hm01b0.py index 83d1bb1..43f3a38 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,16 @@ class HM01B0(DVP_Camera): def __init__( self, + interface, i2c, i2c_address = 0x24, - num_data_pins = 1 + num_data_pins = 1, + xclk_freq = 25_000_000, + continuous = False, + height = None, + width = None, + color_mode = None, + buffer = None, ): """ Initializes the HM01B0 camera with default settings. @@ -249,11 +256,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 = xclk_freq, + 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 +417,33 @@ 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)) + + # 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. 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) 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..8b65713 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 @@ -417,7 +417,7 @@ class OV5640(DVP_Camera): # io direction 0x3017, 0xFF, 0x3018, 0xFF, - _DRIVE_CAPABILITY, 0xC3, + _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 @@ -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..bfe654a --- /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(self): + """ + Returns the default resolution of the camera. + + Returns: + tuple: (height, width) + """ + raise NotImplementedError("Subclass must implement this method") + + def resolution_is_supported(self, height, width): + """ + 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(self): + """ + Returns the default color mode of the camera. + + Returns: + int: Color mode + """ + raise NotImplementedError("Subclass must implement this method") + + def color_mode_is_supported(self, color_mode): + """ + 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: