diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..1eb8b54 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: cnadler86 +#patreon: # Replace with a single Patreon username +#open_collective: # Replace with a single Open Collective username +#ko_fi: # Replace with a single Ko-fi username +#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +#liberapay: # Replace with a single Liberapay username +#issuehunt: # Replace with a single IssueHunt username +#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +#polar: # Replace with a single Polar username +#buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +#thanks_dev: # Replace with a single thanks.dev username +#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..cf131bf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,26 @@ +--- +name: Bug report +about: Report a bug and help improving the API +title: '' +labels: bug +assignees: cnadler86 + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Code to reproduce + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Environment:** + - Board: e.g. ESP32S3 XIAO + - Camera drivers version: e.g. 2.0.15 + +**Additional context** +Add any other context about the problem here. + +Note: Also disconnect any peripherals to the board and try without them before raising a bug. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/general-issue.md b/.github/ISSUE_TEMPLATE/general-issue.md new file mode 100644 index 0000000..3e6d63d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/general-issue.md @@ -0,0 +1,11 @@ +--- +name: General issue +about: General issue +title: '' +labels: '' +assignees: '' + +--- + +PLEASE, read the documentation, search [previous issues](https://github.com/cnadler86/micropython-camera-API/issues?q=is%3Aissue) and/or ask ChatGPT before! +Also disconnect any peripherals to the board and try without them before raising an issue. diff --git a/.github/workflows/Create-Release.yml b/.github/workflows/Create-Release.yml deleted file mode 100644 index 4b8dccf..0000000 --- a/.github/workflows/Create-Release.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Add-Artifacts-to-Draft-Release - -on: - release: - types: - - created - -jobs: - add_artifacts: - if: ${{ github.event.release.draft }} - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up GitHub CLI - uses: actions/setup-gh-cli@v2 - - - name: Authenticate GitHub CLI - run: echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token - - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - name: firmware-* - path: ./artifacts - - - name: Upload Release Assets - run: | - for file in ./artifacts/*; do - gh release upload "${{ github.event.release.tag_name }}" "$file" --clobber - done diff --git a/.github/workflows/ESP32.yml b/.github/workflows/ESP32.yml index 9548001..3ad8407 100644 --- a/.github/workflows/ESP32.yml +++ b/.github/workflows/ESP32.yml @@ -3,9 +3,13 @@ name: ESP32 on: workflow_dispatch: push: + branches: + - '**' paths: - 'src/**' - '.github/workflows/*.yml' + tags-ignore: + - 'v*' pull_request: branches: - master @@ -49,16 +53,39 @@ jobs: sudo apt-get update sudo apt-get install -y git wget flex bison gperf python3 python3-pip python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 + # Download and set up ESP-IDF (if not cached) + - name: Set up ESP-IDF + id: export-idf + if: steps.cache_esp_idf.outputs.cache-hit != 'true' + run: | + cd ~ + git clone --depth 1 --branch v5.4.2 https://github.com/espressif/esp-idf.git + # git clone --depth 1 --branch ${{ env.IDF_VER }} https://github.com/espressif/esp-idf.git + git -C esp-idf submodule update --init --recursive --filter=tree:0 + cd esp-idf + ./install.sh all + cd components + # latest_cam_driver=$(curl -s https://api.github.com/repos/espressif/esp32-camera/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + # git clone --depth 1 --branch $latest_cam_driver https://github.com/espressif/esp32-camera.git + git clone https://github.com/cnadler86/esp32-camera.git + cd ~/esp-idf/ + source ./export.sh + cd ~ + git clone https://github.com/espressif/esp-adf-libs.git + cp -r ~/esp-adf-libs/esp_new_jpeg ~/esp-idf/components/ + # Clone the latest MicroPython release (if not cached) - name: Clone MicroPython latest release id: clone-micropython if: steps.cache_esp_idf.outputs.cache-hit != 'true' run: | echo "Cloning MicroPython release: $MPY_RELEASE" + cd ~/esp-idf/ + source ./export.sh cd ~ git clone --depth 1 --branch ${{ env.MPY_RELEASE }} https://github.com/micropython/micropython.git cd micropython - git submodule update --init --depth 1 + # git submodule update --init --depth 1 cd mpy-cross make cd ~/micropython/ports/esp32 @@ -66,22 +93,6 @@ jobs: echo "Micropython setup successfully" source ~/micropython/tools/ci.sh && echo "IDF_VER=$IDF_VER" >> $GITHUB_ENV - # Download and set up ESP-IDF (if not cached) - - name: Set up ESP-IDF - id: export-idf - if: steps.cache_esp_idf.outputs.cache-hit != 'true' - run: | - cd ~ - # git clone --depth 1 --branch release/v5.2 https://github.com/espressif/esp-idf.git - git clone --depth 1 --branch ${{ env.IDF_VER }} https://github.com/espressif/esp-idf.git - git -C esp-idf submodule update --init --recursive --filter=tree:0 - cd esp-idf - ./install.sh all - cd components - git clone https://github.com/cnadler86/esp32-camera - cd ~/esp-idf/ - source ./export.sh - # Dynamically create jobs for each board build: needs: setup-environment @@ -94,7 +105,6 @@ jobs: - ESP32_GENERIC_S2 - ESP32_GENERIC_S3 - ESP32_GENERIC_S3-SPIRAM_OCT - - ESP32_GENERIC_S3-FLASH_4M - ESP32_GENERIC-SPIRAM@WROVER_KIT - ESP32_GENERIC-SPIRAM@ESP_EYE - ESP32_GENERIC-SPIRAM@M5STACK_PSRAM @@ -106,6 +116,7 @@ jobs: - ESP32_GENERIC-SPIRAM@TTGO_T_JOURNAL - ESP32_GENERIC-SPIRAM@TTGO_T_CAMERA_PLUS - ESP32_GENERIC_S3-SPIRAM_OCT@M5STACK_CAMS3_UNIT + - ESP32_GENERIC_S3-SPIRAM_OCT@M5STACK_ATOM_S3R - ESP32_GENERIC_S3-SPIRAM_OCT@XIAO_ESP32S3 - ESP32_GENERIC_S3-SPIRAM_OCT@ESP32S3_CAM_LCD - ESP32_GENERIC_S3-SPIRAM_OCT@ESP32S3_EYE @@ -145,6 +156,11 @@ jobs: # Build MicroPython for each board - name: Build MicroPython run: | + cd ${{ github.workspace }} + cd .. + git clone https://github.com/cnadler86/mp_jpeg.git + cd ~/esp-idf/components/esp32-camera + CAM_DRIVER=$(git describe --tags --always --dirty) cd ~/micropython/ports/esp32 source ~/esp-idf/export.sh @@ -153,9 +169,9 @@ jobs: IFS='-' read -r BOARD_NAME BOARD_VARIANT <<< "${BUILD_TARGET}" if [ -n "${BOARD_VARIANT}" ]; then - IDF_CMD="idf.py -D MICROPY_BOARD=$BOARD_NAME -D USER_C_MODULES=${{ github.workspace }}/src/micropython.cmake -D MICROPY_BOARD_VARIANT=$BOARD_VARIANT -B build-$BUILD_TARGET" + IDF_CMD="idf.py -D MICROPY_BOARD=$BOARD_NAME -D USER_C_MODULES=${{ github.workspace }}/src/micropython.cmake -D MICROPY_BOARD_VARIANT=$BOARD_VARIANT -B build-$BUILD_TARGET -D MP_CAMERA_DRIVER_VERSION=$CAM_DRIVER" else - IDF_CMD="idf.py -D MICROPY_BOARD=$BOARD_NAME -D USER_C_MODULES=${{ github.workspace }}/src/micropython.cmake -B build-$BUILD_TARGET" + IDF_CMD="idf.py -D MICROPY_BOARD=$BOARD_NAME -D USER_C_MODULES=${{ github.workspace }}/src/micropython.cmake -B build-$BUILD_TARGET -D MP_CAMERA_DRIVER_VERSION=$CAM_DRIVER" fi if [ -n "${CAMERA_MODEL}" ]; then echo "FW_NAME=${CAMERA_MODEL}" >> $GITHUB_ENV @@ -175,6 +191,6 @@ jobs: - name: Upload firmware artifact uses: actions/upload-artifact@v4 with: - name: firmware-${{ env.FW_NAME }} + name: mpy_cam-${{ env.MPY_RELEASE }}-${{ env.FW_NAME }} path: ~/artifacts/** retention-days: 5 \ No newline at end of file diff --git a/README.md b/README.md index afba7ee..3768354 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,61 @@ [![ESP32 Port](https://github.com/cnadler86/micropython-camera-API/actions/workflows/ESP32.yml/badge.svg)](https://github.com/cnadler86/micropython-camera-API/actions/workflows/ESP32.yml) -This project aims to support various cameras on different MicroPython ports, starting with the ESP32 port and Omnivision (OV2640 & OV5640) cameras. The project implements a general API for cameras in micropython (such as circuitpython have done). -At the moment, this is a micropython user module, but it might get in the micropython repo in the future. -The API is stable, but it might change without previous announce. - -## Precomiled FW (the easy way) +This project aims to support various cameras (e.g. OV2640, OV5640) on different MicroPython ports, starting with the ESP32 port. The project implements a general API, has precompiled firmware images and supports a lot of cameras out of the box. Defaults are set to work with the OV2640. + +The API is stable, but it might change. Please look into the release section for the latest changes. + +I tied to make things as easy as possible. If you find this project useful, please consider donating to support my work. Thanks! +[![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://www.paypal.me/cnadler) + +If you want to play arround with AI, take a look at the [micropython binding for esp-dl](https://github.com/cnadler86/mp_esp_dl_models). + +## Content + +- [Precompiled firmware (the easy way)](#Precompiled-firmware-the-easy-way) +- [Using the API](#using-the-api) + - [Importing the camera module](#importing-the-camera-module) + - [Creating a camera object](#creating-a-camera-object) + - [Initializing the camera](#initializing-the-camera) + - [Capture image](#capture-image) + - [Camera reconfiguration](#camera-reconfiguration) + - [Freeing the buffer](#freeing-the-buffer) + - [Is a frame available](#is-frame-available) + - [Additional methods](#additional-methods) + - [Additional information](#additional-information) +- [Build your custom firmware](#build-your-custom-firmware) + - [Setting up the build environment (DIY method)](#setting-up-the-build-environment-diy-method) + - [Add camera configurations to your board (optional, but recommended)](#add-camera-configurations-to-your-board-optional-but-recommended) + - [Build the API](#build-the-api) +- [Notes](#notes) +- [Benchmark](#benchmark) +- [Troubleshooting](#troubleshooting) +- [Donate](#donate) + +## Precompiled firmware (the easy way) If you are not familiar with building custom firmware, visit the [releases](https://github.com/cnadler86/micropython-camera-API/releases) page to download firmware that suits your board. **There are over 20 precompiled board images with the latest micropython!** +To flash the firmware, you can use for example [https://esp.huhn.me/](https://esp.huhn.me/). + +These firmware binaries also include the [mp_jpeg modul](https://github.com/cnadler86/mp_jpeg) to encode/decode JPEGs. ## Using the API -### Importing the Camera Module +### Importing the camera module + +There general way of using the api is as follow: ```python from camera import Camera, GrabMode, PixelFormat, FrameSize, GainCeiling ``` -### Creating a Camera Object +There is also a camera class with asyncio support! To use it, you need to import from the acamera package: + +```python +from acamera import Camera, GrabMode, PixelFormat, FrameSize, GainCeiling +``` + +### Creating a camera object Camera construction using defaults. This is the case if you are using a **non-generic** precompiled firmware or if you specified the camera model or pins in mpconfigboard.h during your build. Then you can just call the construction without any keyword arguments. @@ -58,31 +96,30 @@ cam = Camera( - data_pins: List of data pins - pclk_pin: Pixel clock pin - -vsync_pin: VSYNC pin +- vsync_pin: VSYNC pin - href_pin: HREF pin - sda_pin: SDA pin - scl_pin: SCL pin -- xclk_pin: XCLK pin -- xclk_freq: XCLK frequency in Hz -- powerdown_pin: Powerdown pin (default: -1, meaning not used) -- reset_pin: Reset pin (default: -1, meaning not used) +- xclk_pin: XCLK pin ( set to -1, if you have an external clock source) +- xclk_freq: XCLK frequency in Hz (consult the camera sensor specification) +- powerdown_pin: Powerdown pin (set to -1 if not used) +- reset_pin: Reset pin (set to -1 if not used) - pixel_format: Pixel format as PixelFormat - frame_size: Frame size as FrameSize - jpeg_quality: JPEG quality - fb_count: Frame buffer count - grab_mode: Grab mode as GrabMode - init: Initialize camera at construction time (default: True) -- bmp_out: Image capture output converted to bitmap (default: False) **Default values:** The following keyword arguments have default values: -- xclk_freq: 20MHz // Frequencies are normally either 10 MHz or 20 MHz +- xclk_freq: 20MHz // Default for OV2640 (normally either 10 MHz or 20 MHz). Plase adapt it to your camera sensor. - frame_size: QQVGA - pixel_format: RGB565 - jpeg_quality: 85 // Quality of JPEG output in percent. Higher means higher quality. -- powerdown_pin and reset_pin: -1 (not used/available/needed) +- powerdown_pin and reset_pin: -1 ( = not used/available/needed) - fb_count: - 2 for ESP32S3 boards - 1 for all other @@ -90,7 +127,7 @@ The following keyword arguments have default values: - LATEST for ESP32S3 boards - WHEN_EMPTY for all other -### Initializing the Camera +### Initializing the camera ```python cam.init() @@ -98,13 +135,22 @@ cam.init() ### Capture image +The general way of capturing an image is calling the `capture` method: + ```python img = cam.capture() ``` -Keyword arguments for capture +Each time you call the method, you will receive a new frame as memoryview. +You can convert it to bytes and free the memoryview buffer, so a new frame can be pushed to it. This will reduce the image latency but need more RAM. (see [freeing the buffer](#freeing-the-buffer)) -- out_format: Output format as PixelFormat (optional) +The probably better way of capturing an image would be in an asyncio-loop: + +```python +img = await cam.acapture() #To access this method, you need to import from acamera +``` + +Please consult the [asyncio documentation](https://docs.micropython.org/en/latest/library/asyncio.html), if you have questions on this. ### Camera reconfiguration @@ -119,22 +165,55 @@ Keyword arguments for reconfigure - grab_mode: Grab mode as GrabMode (optional) - fb_count: Frame buffer count (optional) -### Additional methods +### Freeing the buffer + +This is optional, but can reduce the latency of capturing an image in some cases (especially with fb_count = 1).. + +```python +Img = bytes(cam.capture()) #Create a new bytes object from the memoryview (because we want to free it afterwards) +cam.free_buffer() # This will free the captured image or in other words "deleting"" the memoryview +``` + +### Is frame available + +```python +Img = bytes(cam.capture()) +cam.free_buffer() +while not cam.frame_available(): + +print('The frame is available now. You can grab the image by the capture method =)') +``` + +This gives you the possibility of creating an asynchronous application without using asyncio. + +### Additional methods and examples Here are just a few examples: ```python cam.set_quality(90) # The quality goes from 0% to 100%, meaning 100% is the highest but has probably no compression -cam.set_bmp_out(True) # Enables convertion to bmp when capturing image camera.get_brightness() camera.set_vflip(True) #Enable vertical flip ``` See autocompletions in Thonny in order to see the list of methods. -If you want more insides in the methods and what they actually do, you can find a very good documentation [here](https://docs.circuitpython.org/en/latest/shared-bindings/espcamera/index.html). +If you want more insights in the methods and what they actually do, you can find a very good documentation [here](https://docs.circuitpython.org/en/latest/shared-bindings/espcamera/index.html). Note that each method requires a "get_" or "set_" prefix, depending on the desired action. -## Build your custom FW +Take also a look in the examples folder. + +To get the version of the camera driver used: + +```python +import camera +vers = camera.Version() +``` + +### Additional information + +The firmware images support the following cameras out of the box, but is therefore big: OV7670, OV7725, OV2640, OV3660, OV5640, NT99141, GC2145, GC032A, GC0308, BF3005, BF20A6, SC030IOT + +## Build your custom firmware ### Setting up the build environment (DIY method) @@ -142,20 +221,20 @@ To build the project, follow these instructions: - [ESP-IDF](https://docs.espressif.com/projects/esp-idf/en/v5.2.3/esp32/get-started/index.html): I used version 5.2.3, but it might work with other versions (see notes). - Clone the micropython repo and this repo in a folder, e.g. "MyESPCam". MicroPython version 1.24 or higher is required (at least commit 92484d8). -- You will have to add the ESP32-Camera driver (I used v2.0.13). To do this, add the following to the respective idf_component.yml file (e.g. in micropython/ports/esp32/main_esp32s3/idf_component.yml): +- You will have to add the ESP32-Camera driver from my fork. To do this, add the following to the respective idf_component.yml file (e.g. in micropython/ports/esp32/main_esp32s3/idf_component.yml): ```yml espressif/esp32-camera: - git: https://github.com/cnadler86/esp32-camera #At the moment I maintain a fork because of some unsolved bugs and conveniance. + git: https://github.com/cnadler86/esp32-camera.git ``` Alternatively, you can clone the repository inside the esp-idf/components folder instead of altering the idf_component.yml file. ### Add camera configurations to your board (optional, but recommended) -#### Supported Camera Models +#### Supported camera models -This project supports various camera models out of the box. You typically only need to add a single line to your board config file ("mpconfigboard.h). +This project supports various boards with camera interface out of the box. You typically only need to add a single line to your board config file ("mpconfigboard.h). Example (don't forget to add the empty line at the bottom): ```c @@ -173,6 +252,7 @@ Below is a list of supported `MICROPY_CAMERA_MODEL_xxx` definitions: - MICROPY_CAMERA_MODEL_M5STACK_WIDE - [M5Stack Wide](https://shop.m5stack.com/collections/m5-cameras) - MICROPY_CAMERA_MODEL_M5STACK_ESP32CAM - [M5Stack ESP32CAM](https://shop.m5stack.com/collections/m5-cameras) - MICROPY_CAMERA_MODEL_M5STACK_CAMS3_UNIT - [M5Stack CAMS3 Unit](https://shop.m5stack.com/collections/m5-cameras) +- MICROPY_CAMERA_MODEL_M5STACK_ATOM_S3R - [M5Stack Atom S3R](https://docs.m5stack.com/en/core/AtomS3R-M12) - MICROPY_CAMERA_MODEL_AI_THINKER - [AI-Thinker ESP32-CAM] - MICROPY_CAMERA_MODEL_XIAO_ESP32S3 - [XIAO ESP32S3](https://www.seeedstudio.com/xiao-series-page) - MICROPY_CAMERA_MODEL_ESP32_MP_CAMERA_BOARD - [ESP32 MP Camera Board] @@ -188,7 +268,6 @@ Below is a list of supported `MICROPY_CAMERA_MODEL_xxx` definitions: #### For unsupported camera models If your board is not yet supported, add the following lines to your board config-file "mpconfigboard.h" with the respective pins and camera parameters. Otherwise, you will need to pass all parameters during construction. -Don't forget the empty line at the bottom. Example for Xiao sense: ```c @@ -206,14 +285,21 @@ Example for Xiao sense: #define MICROPY_CAMERA_PIN_XCLK (10) #define MICROPY_CAMERA_PIN_PWDN (-1) #define MICROPY_CAMERA_PIN_RESET (-1) -#define MICROPY_CAMERA_PIN_SIOD (40) // SDA -#define MICROPY_CAMERA_PIN_SIOC (39) // SCL +#define MICROPY_CAMERA_PIN_SIOD (40) // SDA +#define MICROPY_CAMERA_PIN_SIOC (39) // SCL #define MICROPY_CAMERA_XCLK_FREQ (20000000) // Frequencies are normally either 10 MHz or 20 MHz -#define MICROPY_CAMERA_FB_COUNT (2) // The value is between 1 (slow) and 2 (fast, but more load on CPU and more ram usage) -#define MICROPY_CAMERA_JPEG_QUALITY (85) // Quality of JPEG output in percent. Higher means higher quality. -#define MICROPY_CAMERA_GRAB_MODE (1) // 0=WHEN_EMPTY (might have old data, but less resources), 1=LATEST (best, but more resources) +#define MICROPY_CAMERA_FB_COUNT (2) // The value is between 1 (slow) and 2 (fast, but more load on CPU and more ram usage) +#define MICROPY_CAMERA_JPEG_QUALITY (85) // Quality of JPEG output in percent. Higher means higher quality. +#define MICROPY_CAMERA_GRAB_MODE (1) // 0=WHEN_EMPTY (might have old data, but less resources), 1=LATEST (best, but more resources) ``` +#### Customize additional camera settings + +If you want to customize additional camera setting or reduce the firmware size by removing support for unused camera sensors, then take a look at the kconfig file of the esp32-camera driver and specify these on the sdkconfig file of your board. + +#### (Optional) Add the mp_jpeg module + +If you also want to include the [mp_jpeg module](https://github.com/cnadler86/mp_jpeg) in your build, clone the mp_jpeg repo at the same level and folder as the mp_camera_api repo and meet the requirements from the mp_jpeg repo. ### Build the API @@ -227,6 +313,7 @@ make USER_C_MODULES=../../../../micropython-camera-API/src/micropython.cmake BOA make USER_C_MODULES=../../../../micropython-camera-API/src/micropython.cmake BOARD= all ``` +Micropython and camera-api folders are at the same level. Note that you need those extra "/../"s while been inside the esp32 port folder. If you experience problems, visit [MicroPython external C modules](https://docs.micropython.org/en/latest/develop/cmodules.html). ## Notes @@ -236,31 +323,37 @@ If you experience problems, visit [MicroPython external C modules](https://docs. - If your target board is a ESP32, I recommend using IDF >= 5.2, since older versions may lead to IRAM overflow during build. Alternatively you can modify your sdkconfig-file (see [issue #1](https://github.com/cnadler86/micropython-camera-API/issues/1)). - The driver requires PSRAM to be installed and activated. - Most of the precompiled firmware images are untested, but the only difference between them are the target architecture and pin definitions, so they should work out of the box. If not, please raise an issue. - -## FPS benchmark - -I didn't use a calibrated osziloscope, but here is a benchmark with my ESP32S3 (GrabMode=LATEST). -Using fb_count=2 doubles the FPS for JPEG. This might also aplly for other PixelFormats. - -| Frame Size | GRAYSCALE | RGB565 | YUV422 | JPEG | JPEG (fb = 2) | -|------------|-----------|--------|--------|--------|---------------| -| R96X96 | 12.5 | 12.5 | 12.5 | No img | No img | -| QQVGA | 12.5 | 12.5 | 12.5 | 25 | 50 | -| QCIF | 11 | 11 | 11.5 | 25 | 50 | -| HQVGA | 12.5 | 12.5 | 12.5 | 25 | 50 | -| R240X240 | 12 | 12.5 | 11.5 | 25 | 50 | -| QVGA | 12 | 11 | 12 | 25 | 50 | -| CIF | 12.5 | No img | No img | 6 | 12.5 | -| HVGA | 2.5 | 3 | 2.5 | 12.5 | 25 | -| VGA | 3 | 3 | 3 | 12.5 | 25 | -| SVGA | 3 | 3 | 3 | 12.5 | 25 | -| XGA | No img | No img | No img | 6 | 12.5 | -| HD | No img | No img | No img | 6 | 12.5 | -| SXGA | 2 | 2 | 2 | 6 | 12.5 | -| UXGA | No img | No img | No img | 6 | 12.5 | - -## Future Plans - -- Edge case: enable usage of pins such as i2c for other applications -- Provide examples in binary image -- Include camera driver version in API +- Every sensor has its own ranges and settings, please consult the respective specifications. + +## Benchmark + +I didn't use a calibrated oscilloscope, but here is a FPS benchmark with my ESP32S3 (xclk_freq = 20MHz, GrabMode=LATEST, fb_count = 1, jpeg_quality=85%) and OV2640. +Using fb_count=2 theoretically can double the FPS (see JPEG with fb_count=2). This might also apply for other PixelFormats. + +| Frame Size | GRAYSCALE | RGB565 | YUV422 | JPEG | JPEG (fb=2) | +|------------|-----------|--------|--------|--------|-------------| +| R96X96 | 12.5 | 12.5 | 12.5 | No img | No img | +| QQVGA | 12.5 | 12.5 | 12.5 | 25 | 50 | +| QCIF | 11 | 11 | 11.5 | 25 | 50 | +| HQVGA | 12.5 | 12.5 | 12.5 | 25 | 50 | +| R240X240 | 12.5 | 12.5 | 11.5 | 25 | 50 | +| QVGA | 12 | 11 | 12 | 25 | 50 | +| CIF | 12.5 | No img | No img | 6.3 | 12.5 | +| HVGA | 3 | 3 | 2.5 | 12.5 | 25 | +| VGA | 3 | 3 | 3 | 12.5 | 25 | +| SVGA | 3 | 3 | 3 | 12.5 | 25 | +| XGA | No img | No img | No img | 6.3 | 12.5 | +| HD | No img | No img | No img | 6.3 | 12.5 | +| SXGA | 2 | 2 | 2 | 6.3 | 12.5 | +| UXGA | No img | No img | No img | 6.3 | 12.5 | + +## Troubleshooting + +You can find information on the following sites: +- [ESP-FAQ](https://docs.espressif.com/projects/esp-faq/en/latest/application-solution/camera-application.html) +- [ChatGPT](https://chatgpt.com/) +- [Issues in here](https://github.com/cnadler86/micropython-camera-API/issues?q=is%3Aissue) + +## Donate + +If you enjoy this work and would like to share your enjoyment, please feel free to [donate](https://github.com/sponsors/cnadler86?frequency=one-time) and contribute to the project. Thanks! :blush: diff --git a/examples/CameraSettings.html b/examples/CameraSettings.html index 27e7447..e392005 100644 --- a/examples/CameraSettings.html +++ b/examples/CameraSettings.html @@ -102,8 +102,8 @@ function populateFrameSizeDropdown() { const frameSizes = [ - "R96x96", "QQVGA", "CIF", "HQVGA", "R240x240", "QVGA", - "CIF", "HVGA", "VGA", "SVGA", "XGA", "HD", "SXGA", + "R96x96", "QQVGA", "128X128", "CIF", "HQVGA", "R240x240", "QVGA", + "320X320","CIF", "HVGA", "VGA", "SVGA", "XGA", "HD", "SXGA", "UXGA", "FHD", "P_HD", "P_3MP", "QXGA", "QHD", "WQXGA", "P_FHD", "QSXGA" ]; @@ -133,7 +133,7 @@ fetch('/get_sensor_name') .then(response => response.text()) .then(sensorName => { - const showSharpnessAndDenoise = (sensorName === 'OV3640' || sensorName === 'OV5640'); + const showSharpnessAndDenoise = (sensorName === 'OV3660' || sensorName === 'OV5640'); document.getElementById('sharpness-container').classList.toggle('hidden', !showSharpnessAndDenoise); document.getElementById('denoise-container').classList.toggle('hidden', !showSharpnessAndDenoise); }) @@ -170,32 +170,39 @@

Micropython Camera Stream

+
+ + +
@@ -218,11 +225,11 @@

Micropython Camera Stream

diff --git a/examples/CameraSettings.py b/examples/CameraSettings.py index 26512c5..77d6caa 100644 --- a/examples/CameraSettings.py +++ b/examples/CameraSettings.py @@ -1,7 +1,7 @@ import network import asyncio import time -from camera import Camera, FrameSize, PixelFormat +from acamera import Camera, FrameSize, PixelFormat # Import the async version of the Camera class, you can also use the sync version (camera.Camera) cam = Camera(frame_size=FrameSize.VGA, pixel_format=PixelFormat.JPEG, jpeg_quality=85, init=False) # WLAN config @@ -26,19 +26,15 @@ async def stream_camera(writer): try: cam.init() - if not cam.get_bmp_out() and cam.get_pixel_format() != PixelFormat.JPEG: - cam.set_bmp_out(True) - + await asyncio.sleep(1) + writer.write(b'HTTP/1.1 200 OK\r\nContent-Type: multipart/x-mixed-replace; boundary=frame\r\n\r\n') await writer.drain() while True: - frame = cam.capture() + frame = await cam.acapture() # This is the async version of capture, you can also use frame = cam.capture() instead if frame: - if cam.get_pixel_format() == PixelFormat.JPEG: - writer.write(b'--frame\r\nContent-Type: image/jpeg\r\n\r\n') - else: - writer.write(b'--frame\r\nContent-Type: image/bmp\r\n\r\n') + writer.write(b'--frame\r\nContent-Type: image/jpeg\r\n\r\n') writer.write(frame) await writer.drain() @@ -68,7 +64,14 @@ async def handle_client(reader, writer): writer.write(response.encode()) await writer.drain() else: - response = 'HTTP/1.1 404 Not Found\r\n\r\n' + try: + cam.reconfigure(**{method_name: value}) + print(f"Camera reconfigured with {method_name}={value}") + print("This action restores all previous configuration!") + response = 'HTTP/1.1 200 OK\r\n\r\n' + except Exception as e: + print(f"Error with {method_name}: {e}") + response = 'HTTP/1.1 404 Not Found\r\n\r\n' writer.write(response.encode()) await writer.drain() diff --git a/examples/benchmark.py b/examples/benchmark.py index 806a204..1ba7aac 100644 --- a/examples/benchmark.py +++ b/examples/benchmark.py @@ -5,20 +5,21 @@ gc.enable() def measure_fps(duration=2): - start_time = time.time() - while time.time() - start_time < 0.5: + start_time = time.ticks_ms() + while time.ticks_ms() - start_time < 500: cam.capture() - start_time = time.time() + start_time = time.ticks_ms() frame_count = 0 - while time.time() - start_time < duration: - cam.capture() - frame_count += 1 + while time.ticks_ms() - start_time < duration*1000: + img = cam.capture() + if img: + frame_count += 1 - end_time = time.time() - fps = frame_count / (end_time - start_time) - return fps + end_time = time.ticks_ms() + fps = frame_count / (end_time - start_time) * 1000 + return round(fps,1) def print_summary_table(results, cam): print(f"\nBenchmark {os.uname().machine} with {cam.get_sensor_name()}, GrabMode: {cam.get_grab_mode()}:") @@ -78,7 +79,7 @@ def print_summary_table(results, cam): print('Set', p, f,f'fb={fb}',':') try: - cam.reconfigure(frame_size=f_value) + cam.reconfigure(frame_size=f_value) #set_frame_size fails for YUV422 time.sleep_ms(10) img = cam.capture() diff --git a/examples/benchmark_img_conv.py b/examples/benchmark_img_conv.py new file mode 100644 index 0000000..9a34b64 --- /dev/null +++ b/examples/benchmark_img_conv.py @@ -0,0 +1,107 @@ +from camera import Camera, FrameSize, PixelFormat +import time +import gc +import os +gc.enable() + +def measure_fps(cam,out_fmt,duration=2): + start_time = time.ticks_ms() + while time.ticks_ms() - start_time < 500: + cam.capture(out_fmt) + + start_time = time.ticks_ms() + frame_count = 0 + + while time.ticks_ms() - start_time < duration*1000: + img = cam.capture(out_fmt) + if img: + frame_count += 1 + + end_time = time.ticks_ms() + fps = frame_count / (end_time - start_time) * 1000 + return round(fps,1) + +def print_summary_table(results, cam): + print(f"\nBenchmark {os.uname().machine} with {cam.get_sensor_name()}, GrabMode: {cam.get_grab_mode()}:") + + fb_counts = sorted(results.keys()) + frame_size_names = {getattr(FrameSize, f): f for f in dir(FrameSize) if not f.startswith('_')} + + header_row = f"{'Frame Size':<15}" + sub_header_row = " " * 15 + + for fb in fb_counts: + for p in results[fb].keys(): + header_row += f"{'fb_count ' + str(fb):<15}" + sub_header_row += f"{p:<15}" + + print(header_row) + print(sub_header_row) + + frame_sizes = list(next(iter(next(iter(results.values())).values())).keys()) + + for f in frame_sizes: + frame_size_name = frame_size_names.get(f, str(f)) + print(f"{frame_size_name:<15}", end="") + + for fb in fb_counts: + for p in results[fb].keys(): + fps = results[fb][p].get(f, "N/A") + print(f"{fps:<15}", end="") + print() + +if __name__ == "__main__": + cam = Camera(pixel_format=PixelFormat.JPEG) + results = {} + + try: + for fb in [1, 2]: + cam.reconfigure(fb_count=fb, frame_size=FrameSize.QQVGA) + results[fb] = {} + for p in dir(PixelFormat): + if not p.startswith('_'): + p_value = getattr(PixelFormat, p) + try: + if p_value == PixelFormat.JPEG: + continue + cam.capture(p_value) + results[fb][p] = {} + gc.collect() + except: + continue + for f in dir(FrameSize): + if not f.startswith('_'): + f_value = getattr(FrameSize, f) + if f_value > cam.get_max_frame_size(): + continue + gc.collect() + print('Set', p, f,f'fb={fb}',':') + + try: + cam.set_frame_size(f_value) + time.sleep_ms(10) + img = cam.capture(p_value) + if img: + print('---> Image size:', len(img)) + fps = measure_fps(cam,p_value,2) + print(f"---> FPS: {fps}") + results[fb][p][f_value] = fps + else: + print('No image captured') + results[fb][p][f_value] = 'No img' + + print(f"---> Free Memory: {gc.mem_free()}") + except Exception as e: + print('ERR:', e) + results[fb][p][f_value] = 'ERR' + finally: + time.sleep_ms(250) + gc.collect() + print('') + + except KeyboardInterrupt: + print("\nScript interrupted by user.") + + finally: + print_summary_table(results, cam) + cam.deinit() diff --git a/src/acamera.py b/src/acamera.py new file mode 100644 index 0000000..46c2a6f --- /dev/null +++ b/src/acamera.py @@ -0,0 +1,12 @@ +import asyncio +from camera import Camera as _Camera +from camera import FrameSize, PixelFormat, GainCeiling, GrabMode + +class Camera(_Camera): + async def acapture(self): + self.free_buffer() # Free the buffer so the camera task grabs a new frame + while not self.frame_available(): + await asyncio.sleep(0) # Yield control to the event loop + return self.capture() + +__all__ = ['Camera', 'FrameSize', 'PixelFormat', 'GainCeiling', 'GrabMode'] \ No newline at end of file diff --git a/src/camera_pins.h b/src/camera_pins.h index 203f7f1..6041cae 100644 --- a/src/camera_pins.h +++ b/src/camera_pins.h @@ -116,6 +116,25 @@ #define MICROPY_CAMERA_PIN_HREF 18 #define MICROPY_CAMERA_PIN_PCLK 12 +#elif defined(MICROPY_CAMERA_MODEL_M5STACK_ATOM_S3R) +#define MICROPY_CAMERA_PIN_PWDN -1 //needs to be low to run +#define MICROPY_CAMERA_PIN_RESET -1 +#define MICROPY_CAMERA_PIN_XCLK 21 +#define MICROPY_CAMERA_PIN_SIOD 12 +#define MICROPY_CAMERA_PIN_SIOC 9 + +#define MICROPY_CAMERA_PIN_D7 13 +#define MICROPY_CAMERA_PIN_D6 11 +#define MICROPY_CAMERA_PIN_D5 17 +#define MICROPY_CAMERA_PIN_D4 4 +#define MICROPY_CAMERA_PIN_D3 48 +#define MICROPY_CAMERA_PIN_D2 46 +#define MICROPY_CAMERA_PIN_D1 42 +#define MICROPY_CAMERA_PIN_D0 3 +#define MICROPY_CAMERA_PIN_VSYNC 10 +#define MICROPY_CAMERA_PIN_HREF 14 +#define MICROPY_CAMERA_PIN_PCLK 40 + #elif defined(MICROPY_CAMERA_MODEL_AI_THINKER) #define MICROPY_CAMERA_PIN_PWDN 32 #define MICROPY_CAMERA_PIN_RESET -1 diff --git a/src/manifest.py b/src/manifest.py new file mode 100644 index 0000000..ff69f76 --- /dev/null +++ b/src/manifest.py @@ -0,0 +1,4 @@ +# Include the board's default manifest. +include("$(PORT_DIR)/boards/manifest.py") +# Add custom driver +module("acamera.py") \ No newline at end of file diff --git a/src/micropython.cmake b/src/micropython.cmake index 68c68c8..178d39f 100644 --- a/src/micropython.cmake +++ b/src/micropython.cmake @@ -1,26 +1,93 @@ -# `py.cmake` for `micropy_gather_target_properties` macro usage include(${MICROPY_DIR}/py/py.cmake) +set(MICROPY_FROZEN_MANIFEST ${CMAKE_CURRENT_LIST_DIR}/manifest.py) + add_library(usermod_mp_camera INTERFACE) +add_dependencies(usermod_mp_camera esp32_camera) + target_sources(usermod_mp_camera INTERFACE ${CMAKE_CURRENT_LIST_DIR}/modcamera.c ${CMAKE_CURRENT_LIST_DIR}/modcamera_api.c ) -target_include_directories(usermod_mp_camera INTERFACE - ${CMAKE_CURRENT_LIST_DIR} - ${IDF_PATH}/components/esp32-camera/driver/include - ${IDF_PATH}/components/esp32-camera/driver/private_include - ${IDF_PATH}/components/esp32-camera/conversions/include - ${IDF_PATH}/components/esp32-camera/conversions/private_include - ${IDF_PATH}/components/esp32-camera/sensors/private_include -) +# Prefer user-defined ESP32_CAMERA_DIR if provided +if(DEFINED ESP32_CAMERA_DIR AND EXISTS "${ESP32_CAMERA_DIR}") + message(STATUS "Using user-defined ESP32 Camera directory: ${ESP32_CAMERA_DIR}") + + target_include_directories(usermod_mp_camera INTERFACE + ${CMAKE_CURRENT_LIST_DIR} + ${ESP32_CAMERA_DIR}/driver/include + ${ESP32_CAMERA_DIR}/conversions/include + ${ESP32_CAMERA_DIR}/driver/private_include + ${ESP32_CAMERA_DIR}/conversions/private_include + ${ESP32_CAMERA_DIR}/sensors/private_include + ) + +# If no manual directory is provided, try to fetch it from ESP-IDF +elseif(EXISTS ${IDF_PATH}/components/esp32-camera) + idf_component_get_property(CAMERA_INCLUDES esp32-camera INCLUDE_DIRS) + idf_component_get_property(CAMERA_PRIV_INCLUDES esp32-camera PRIV_INCLUDE_DIRS) + idf_component_get_property(CAMERA_DIR esp32-camera COMPONENT_DIR) + + if(CAMERA_DIR) + message(STATUS "Using ESP32 Camera component from ESP-IDF: ${CAMERA_DIR}") + + # Add public include directories from ESP-IDF + if(CAMERA_INCLUDES) + list(TRANSFORM CAMERA_INCLUDES PREPEND ${CAMERA_DIR}/) + target_include_directories(usermod_mp_camera INTERFACE ${CAMERA_INCLUDES}) + endif() + + # Add private include directories from ESP-IDF + if(CAMERA_PRIV_INCLUDES) + list(TRANSFORM CAMERA_PRIV_INCLUDES PREPEND ${CAMERA_DIR}/) + target_include_directories(usermod_mp_camera INTERFACE ${CAMERA_PRIV_INCLUDES}) + endif() + else() + message(WARNING "ESP32 Camera component not found in ESP-IDF!") + target_include_directories(usermod_mp_camera PUBLIC ${CMAKE_CURRENT_LIST_DIR}) + endif() +endif() +# Check if MP_JPEG_DIR is set or if mp_jpeg directory exists two levels up +if(DEFINED MP_JPEG_DIR AND EXISTS "${MP_JPEG_DIR}") + message(STATUS "Using user-defined MP_JPEG_DIR: ${MP_JPEG_DIR}") + set(MP_JPEG_SRC "${MP_JPEG_DIR}/src/micropython.cmake") +elseif(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../../mp_jpeg") + message(STATUS "Found mp_jpeg directory two levels up") + set(MP_JPEG_SRC "${CMAKE_CURRENT_LIST_DIR}/../../mp_jpeg/src/micropython.cmake") +endif() + +# Add MP_JPEG_SRC cmake file to target_sources if it is defined +if(DEFINED MP_JPEG_SRC AND EXISTS "${MP_JPEG_SRC}") + include(${MP_JPEG_SRC}) +else() + message(WARNING "MP_JPEG_SRC not found or not defined!") +endif() + +# Define MICROPY_CAMERA_MODEL if specified if (MICROPY_CAMERA_MODEL) - target_compile_definitions(usermod_mp_camera INTERFACE MICROPY_CAMERA_MODEL_${MICROPY_CAMERA_MODEL}=1) + message(STATUS "Using user-defined camera model: ${MICROPY_CAMERA_MODEL}") + target_compile_definitions(usermod_mp_camera INTERFACE + MICROPY_CAMERA_MODEL_${MICROPY_CAMERA_MODEL}=1 + ) endif() +# Define MP_CAMERA_DRIVER_VERSION if specified +if (MP_CAMERA_DRIVER_VERSION) + target_compile_definitions(usermod_mp_camera INTERFACE + MP_CAMERA_DRIVER_VERSION=\"${MP_CAMERA_DRIVER_VERSION}\" + ) +endif() + +# Camera module strings are not suitable for compression and cause size increase +target_compile_definitions(usermod_mp_camera INTERFACE + MICROPY_ROM_TEXT_COMPRESSION=0 +) + +# Link the camera module with the main usermod target target_link_libraries(usermod INTERFACE usermod_mp_camera) -micropy_gather_target_properties(usermod_mp_camera) \ No newline at end of file +# Gather target properties for MicroPython build system +micropy_gather_target_properties(usermod_mp_camera) diff --git a/src/modcamera.c b/src/modcamera.c index 041554b..5a0bd05 100644 --- a/src/modcamera.c +++ b/src/modcamera.c @@ -28,52 +28,15 @@ #include "modcamera.h" #include "esp_err.h" #include "esp_log.h" -#include "img_converters.h" +#include "mphalport.h" -#define TAG "ESP32_MPY_CAMERA" +#define TAG "MPY_CAMERA" #if !CONFIG_SPIRAM #error Camera only works on boards configured with spiram #endif -// #if !defined(CONFIG_CAMERA_CORE0) && !defined(CONFIG_CAMERA_CORE1) -// #if MP_TASK_COREID == 0 -// #define CONFIG_CAMERA_CORE0 1 -// #elif MP_TASK_COREID == 1 -// #define CONFIG_CAMERA_CORE1 1 -// #endif -// #endif // CONFIG_CAMERA_COREx - -// Supporting functions -static void raise_micropython_error_from_esp_err(esp_err_t err) { - switch (err) { - case ESP_OK: - return; - case ESP_ERR_NO_MEM: - mp_raise_msg(&mp_type_MemoryError, MP_ERROR_TEXT("Out of memory")); - break; - case ESP_ERR_INVALID_ARG: - mp_raise_ValueError(MP_ERROR_TEXT("Invalid argument")); - break; - case ESP_ERR_INVALID_STATE: - mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("Invalid state")); - break; - case ESP_ERR_NOT_FOUND: - mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("Camera not found")); - break; - case ESP_ERR_NOT_SUPPORTED: - mp_raise_NotImplementedError(MP_ERROR_TEXT("Operation/Function not supported/implemented")); - break; - case ESP_ERR_TIMEOUT: - mp_raise_OSError(MP_ETIMEDOUT); - break; - default: - mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("Unknown error 0x%04x"), err); - // mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("Unknown error")); - break; - } -} - +// Helper functions static int map(int value, int fromLow, int fromHigh, int toLow, int toHigh) { if (fromHigh == fromLow) { mp_raise_ValueError(MP_ERROR_TEXT("fromLow und fromHigh shall not be equal")); @@ -85,6 +48,59 @@ static inline int get_mapped_jpeg_quality(int8_t quality) { return map(quality, 0, 100, 63, 0); } +static inline void check_init(mp_camera_obj_t *self) { + if (!self->initialized) { + mp_raise_OSError(ENOENT); + } +} + +static void set_check_xclk_freq(mp_camera_obj_t *self, int32_t xclk_freq_hz) { + if ( xclk_freq_hz > 40000000) { + mp_raise_ValueError(MP_ERROR_TEXT("xclk frequency cannot be grather than 40MHz")); + } else { + self->camera_config.xclk_freq_hz = xclk_freq_hz; + } +} + +static void set_check_fb_count(mp_camera_obj_t *self, mp_int_t fb_count) { + if (fb_count > 2) { + self->camera_config.fb_count = 2; + mp_warning(NULL, "Frame buffer size limited to 2"); + } else if (fb_count < 1) { + self->camera_config.fb_count = 1; + mp_warning(NULL, "Frame buffer size must be >0. Setting it to 1"); + } + else { + self->camera_config.fb_count = fb_count; + } +} + +static void set_check_grab_mode(mp_camera_obj_t *self, mp_camera_grabmode_t grab_mode) { + if (grab_mode != CAMERA_GRAB_WHEN_EMPTY && grab_mode != CAMERA_GRAB_LATEST) { + mp_raise_ValueError(MP_ERROR_TEXT("Invalid grab_mode")); + } else { + self->camera_config.grab_mode = grab_mode; + } +} + +static void set_check_pixel_format(mp_camera_obj_t *self, mp_camera_pixformat_t pixel_format) { + if ( pixel_format > PIXFORMAT_RGB555) { //Maximal enum value, but validation should be better since wrong pixelformat leads to reboot. + mp_raise_ValueError(MP_ERROR_TEXT("Invalid pixel_format")); + } else { + self->camera_config.pixel_format = pixel_format; + } +} + +static bool init_camera(mp_camera_obj_t *self) { + // Correct the quality before it is passed to esp32 driver and then "undo" the correction in the camera_config + int8_t api_jpeg_quality = self->camera_config.jpeg_quality; + self->camera_config.jpeg_quality = get_mapped_jpeg_quality(api_jpeg_quality); + esp_err_t err = esp_camera_init(&self->camera_config); + self->camera_config.jpeg_quality = api_jpeg_quality; + check_esp_err(err); + return true; +} + // Camera HAL Funcitons void mp_camera_hal_construct( mp_camera_obj_t *self, @@ -104,10 +120,6 @@ void mp_camera_hal_construct( int8_t fb_count, mp_camera_grabmode_t grab_mode) { // configure camera based on arguments - self->camera_config.pixel_format = pixel_format; - self->camera_config.frame_size = frame_size; - // self->camera_config.jpeg_quality = (int8_t)map(jpeg_quality,0,100,63,0); //0-63 lower number means higher quality. - self->camera_config.jpeg_quality = jpeg_quality; //save value in here, but will be corrected (with map) before passing it to the esp32-driver self->camera_config.pin_d0 = data_pins[0]; self->camera_config.pin_d1 = data_pins[1]; self->camera_config.pin_d2 = data_pins[2]; @@ -124,27 +136,18 @@ void mp_camera_hal_construct( self->camera_config.pin_xclk = external_clock_pin; self->camera_config.pin_sscb_sda = sccb_sda_pin; self->camera_config.pin_sscb_scl = sccb_scl_pin; - if ( xclk_freq_hz > 20000000) { - mp_raise_ValueError(MP_ERROR_TEXT("xclk frequency cannot be grather than 20MHz")); - } else { - self->camera_config.xclk_freq_hz = xclk_freq_hz; - } - if (fb_count > 3) { - self->camera_config.fb_count = 3; - mp_warning(NULL, "Frame buffer size limited to 3"); - } else if (fb_count < 1) { - self->camera_config.fb_count = 1; - mp_warning(NULL, "Frame buffer size must be >0. Setting it to 1"); - } - else { - self->camera_config.fb_count = fb_count; //if more than one, i2s runs in continuous mode. TODO: Test with others than JPEG - } - self->camera_config.grab_mode = grab_mode; + self->camera_config.frame_size = frame_size; + self->camera_config.jpeg_quality = jpeg_quality; //save value in here, but will be corrected (with map) before passing it to the esp32-driver + + set_check_pixel_format(self, pixel_format); + set_check_xclk_freq(self, xclk_freq_hz); + set_check_fb_count(self, fb_count); + set_check_grab_mode(self, grab_mode); // defaul parameters self->camera_config.fb_location = CAMERA_FB_IN_PSRAM; - self->camera_config.ledc_timer = LEDC_TIMER_0; + self->camera_config.ledc_timer = LEDC_TIMER_3; self->camera_config.ledc_channel = LEDC_CHANNEL_0; self->initialized = false; @@ -161,19 +164,8 @@ void mp_camera_hal_init(mp_camera_obj_t *self) { } #endif ESP_LOGI(TAG, "Initializing camera"); - // Correct the quality before it is passed to esp32 driver and then "undo" the correction in the camera_config - int8_t api_jpeg_quality = self->camera_config.jpeg_quality; - self->camera_config.jpeg_quality = get_mapped_jpeg_quality(api_jpeg_quality); - esp_err_t err = esp_camera_init(&self->camera_config); - self->camera_config.jpeg_quality = api_jpeg_quality; - if (err != ESP_OK) { - self->initialized = false; - raise_micropython_error_from_esp_err(err); - return; - } - self->initialized = true; + self->initialized = init_camera(self); ESP_LOGI(TAG, "Camera initialized successfully"); - return; } void mp_camera_hal_deinit(mp_camera_obj_t *self) { @@ -183,216 +175,123 @@ void mp_camera_hal_deinit(mp_camera_obj_t *self) { self->captured_buffer = NULL; } esp_err_t err = esp_camera_deinit(); - raise_micropython_error_from_esp_err(err); + check_esp_err(err); self->initialized = false; ESP_LOGI(TAG, "Camera deinitialized"); } } void mp_camera_hal_reconfigure(mp_camera_obj_t *self, mp_camera_framesize_t frame_size, mp_camera_pixformat_t pixel_format, mp_camera_grabmode_t grab_mode, mp_int_t fb_count) { - if (self->initialized) { - ESP_LOGI(TAG, "Reconfiguring camera"); - sensor_t *sensor = esp_camera_sensor_get(); - camera_sensor_info_t *sensor_info = esp_camera_sensor_get_info(&sensor->id); - - if (PIXFORMAT_JPEG == self->camera_config.pixel_format && (!sensor_info->support_jpeg)) { - mp_raise_NotImplementedError(MP_ERROR_TEXT("Sensor does not support JPEG")); - } + check_init(self); + ESP_LOGI(TAG, "Reconfiguring camera with frame size: %d, pixel format: %d, grab mode: %d, fb count: %d", (int)frame_size, (int)pixel_format, (int)grab_mode, (int)fb_count); + + sensor_t *sensor = esp_camera_sensor_get(); + camera_sensor_info_t *sensor_info = esp_camera_sensor_get_info(&sensor->id); + if (frame_size > sensor_info->max_size) { + mp_warning(NULL, "Frame size will be scaled down to maximal frame size supported by the camera sensor"); + self->camera_config.frame_size = sensor_info->max_size; + } else { + self->camera_config.frame_size = frame_size; + } - if (frame_size > sensor_info->max_size) { - mp_warning(NULL, "Frame size will be scaled down to maximal frame size supported by the camera sensor"); - self->camera_config.frame_size = sensor_info->max_size; - } else { - self->camera_config.frame_size = frame_size; - } + set_check_pixel_format(self, pixel_format); + set_check_grab_mode(self, grab_mode); + set_check_fb_count(self, fb_count); - if ( pixel_format > PIXFORMAT_RGB555) { //Maximal enum value, but validation should be better since wrong pixelformat leads to reboot. - mp_raise_ValueError(MP_ERROR_TEXT("Invalid pixel_format")); - } else { - self->camera_config.pixel_format = pixel_format; - } - - if (grab_mode != CAMERA_GRAB_WHEN_EMPTY && grab_mode != CAMERA_GRAB_LATEST) { - mp_raise_ValueError(MP_ERROR_TEXT("Invalid grab_mode")); - } else { - self->camera_config.grab_mode = grab_mode; - } - - if (fb_count > 2) { - self->camera_config.fb_count = 2; - mp_warning(NULL, "Frame buffer size limited to 2"); - } else if (fb_count < 1) { - self->camera_config.fb_count = 1; - mp_warning(NULL, "Set to min frame buffer size of 1"); - } - else { - self->camera_config.fb_count = fb_count; - } - - raise_micropython_error_from_esp_err(esp_camera_deinit()); - - // Correct the quality before it is passed to esp32 driver and then "undo" the correction in the camera_config - int8_t api_jpeg_quality = self->camera_config.jpeg_quality; - self->camera_config.jpeg_quality = get_mapped_jpeg_quality(api_jpeg_quality); - esp_err_t err = esp_camera_init(&self->camera_config); - self->camera_config.jpeg_quality = api_jpeg_quality; - - if (err != ESP_OK) { - self->initialized = false; - raise_micropython_error_from_esp_err(err); - } else { - ESP_LOGI(TAG, "Camera reconfigured successfully"); - } - } + check_esp_err(esp_camera_deinit()); + self->initialized = false; + self->initialized = init_camera(self); + ESP_LOGI(TAG, "Camera reconfigured successfully"); } -mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self, int8_t out_format) { - if (!self->initialized) { - mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("Failed to capture image: Camera not initialized")); - } +mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self) { + check_init(self); if (self->captured_buffer) { esp_camera_fb_return(self->captured_buffer); self->captured_buffer = NULL; } - static size_t out_len = 0; - static uint8_t *out_buf = NULL; - if (out_len > 0 || out_buf) { - free(out_buf); - out_len = 0; - out_buf = NULL; - } - ESP_LOGI(TAG, "Capturing image"); self->captured_buffer = esp_camera_fb_get(); if (!self->captured_buffer) { ESP_LOGE(TAG, "Failed to capture image"); return mp_const_none; } - - if (out_format >= 0 && (mp_camera_pixformat_t)out_format != self->camera_config.pixel_format) { - switch ((mp_camera_pixformat_t)out_format) { - case PIXFORMAT_JPEG: - if (frame2jpg(self->captured_buffer, self->camera_config.jpeg_quality, &out_buf, &out_len)) { - esp_camera_fb_return(self->captured_buffer); - mp_obj_t result = mp_obj_new_memoryview('b', out_len, out_buf); - return result; - } else { - return mp_const_none; - } - - case PIXFORMAT_RGB888: - out_len = self->captured_buffer->width * self->captured_buffer->height * 3; - out_buf = (uint8_t *)malloc(out_len); - if (!out_buf) { - ESP_LOGE(TAG, "out_buf malloc failed"); - return mp_const_none; - } - if (fmt2rgb888(self->captured_buffer->buf, self->captured_buffer->len, self->captured_buffer->format, out_buf)) { - esp_camera_fb_return(self->captured_buffer); - mp_obj_t result = mp_obj_new_memoryview('b', out_len, out_buf); - return result; - } else { - return mp_const_none; - } - - case PIXFORMAT_RGB565: - if(self->camera_config.pixel_format == PIXFORMAT_JPEG){ - if (jpg2rgb565(self->captured_buffer->buf, self->captured_buffer->len, out_buf, JPG_SCALE_NONE)) { - esp_camera_fb_return(self->captured_buffer); - mp_obj_t result = mp_obj_new_memoryview('b', out_len, out_buf); - return result; - } else { - return mp_const_none; - } - } else { - mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("Can only convert JPEG to RGB565")); - return mp_const_none; - } - - default: - mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("Unsupported pixel format for conversion")); - return mp_const_none; + return mp_obj_new_memoryview('b', self->captured_buffer->len, self->captured_buffer->buf); - } - } +} - if (self->bmp_out == false) { - ESP_LOGI(TAG, "Returning imgae without conversion"); - return mp_obj_new_memoryview('b', self->captured_buffer->len, self->captured_buffer->buf); - } else { - ESP_LOGI(TAG, "Returning image as bitmap"); - if (frame2bmp(self->captured_buffer, &out_buf, &out_len)) { - esp_camera_fb_return(self->captured_buffer); - mp_obj_t result = mp_obj_new_memoryview('b', out_len, out_buf); - return result; - } else { - free(out_buf); - out_buf = NULL; - out_len = 0; - mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("Failed to convert image to BMP")); - return mp_const_none; - } - } +mp_obj_t mp_camera_hal_frame_available(mp_camera_obj_t *self) { + check_init(self); + return mp_obj_new_bool(esp_camera_available_frames()); } bool mp_camera_hal_initialized(mp_camera_obj_t *self){ return self->initialized; } +void mp_camera_hal_free_buffer(mp_camera_obj_t *self) { + if (self->captured_buffer) { + esp_camera_fb_return(self->captured_buffer); + self->captured_buffer = NULL; + } +} + const mp_rom_map_elem_t mp_camera_hal_pixel_format_table[] = { - { MP_ROM_QSTR(MP_QSTR_JPEG), MP_ROM_INT(PIXFORMAT_JPEG) }, - { MP_ROM_QSTR(MP_QSTR_YUV422), MP_ROM_INT(PIXFORMAT_YUV422) }, - { MP_ROM_QSTR(MP_QSTR_GRAYSCALE), MP_ROM_INT(PIXFORMAT_GRAYSCALE) }, - { MP_ROM_QSTR(MP_QSTR_RGB565), MP_ROM_INT(PIXFORMAT_RGB565) }, - { MP_ROM_QSTR(MP_QSTR_RGB888), MP_ROM_INT(PIXFORMAT_RGB888) }, + { MP_ROM_QSTR(MP_QSTR_JPEG), MP_ROM_INT((mp_uint_t)PIXFORMAT_JPEG) }, + { MP_ROM_QSTR(MP_QSTR_YUV422), MP_ROM_INT((mp_uint_t)PIXFORMAT_YUV422) }, + { MP_ROM_QSTR(MP_QSTR_YUV420), MP_ROM_INT((mp_uint_t)PIXFORMAT_YUV420) }, + { MP_ROM_QSTR(MP_QSTR_GRAYSCALE), MP_ROM_INT((mp_uint_t)PIXFORMAT_GRAYSCALE) }, + { MP_ROM_QSTR(MP_QSTR_RGB565), MP_ROM_INT((mp_uint_t)PIXFORMAT_RGB565) }, + { MP_ROM_QSTR(MP_QSTR_RGB888), MP_ROM_INT((mp_uint_t)PIXFORMAT_RGB888) }, + { MP_ROM_QSTR(MP_QSTR_RAW), MP_ROM_INT((mp_uint_t)PIXFORMAT_RAW) }, + { MP_ROM_QSTR(MP_QSTR_RGB444), MP_ROM_INT((mp_uint_t)PIXFORMAT_RGB444) }, + { MP_ROM_QSTR(MP_QSTR_RGB555), MP_ROM_INT((mp_uint_t)PIXFORMAT_RGB555) }, }; const mp_rom_map_elem_t mp_camera_hal_frame_size_table[] = { - { MP_ROM_QSTR(MP_QSTR_R96X96), MP_ROM_INT(FRAMESIZE_96X96) }, - { MP_ROM_QSTR(MP_QSTR_QQVGA), MP_ROM_INT(FRAMESIZE_QQVGA) }, - { MP_ROM_QSTR(MP_QSTR_QCIF), MP_ROM_INT(FRAMESIZE_QCIF) }, - { MP_ROM_QSTR(MP_QSTR_HQVGA), MP_ROM_INT(FRAMESIZE_HQVGA) }, - { MP_ROM_QSTR(MP_QSTR_R240X240), MP_ROM_INT(FRAMESIZE_240X240) }, - { MP_ROM_QSTR(MP_QSTR_QVGA), MP_ROM_INT(FRAMESIZE_QVGA) }, - { MP_ROM_QSTR(MP_QSTR_CIF), MP_ROM_INT(FRAMESIZE_CIF) }, - { MP_ROM_QSTR(MP_QSTR_HVGA), MP_ROM_INT(FRAMESIZE_HVGA) }, - { MP_ROM_QSTR(MP_QSTR_VGA), MP_ROM_INT(FRAMESIZE_VGA) }, - { MP_ROM_QSTR(MP_QSTR_SVGA), MP_ROM_INT(FRAMESIZE_SVGA) }, - { MP_ROM_QSTR(MP_QSTR_XGA), MP_ROM_INT(FRAMESIZE_XGA) }, - { MP_ROM_QSTR(MP_QSTR_HD), MP_ROM_INT(FRAMESIZE_HD) }, - { MP_ROM_QSTR(MP_QSTR_SXGA), MP_ROM_INT(FRAMESIZE_SXGA) }, - { MP_ROM_QSTR(MP_QSTR_UXGA), MP_ROM_INT(FRAMESIZE_UXGA) }, - { MP_ROM_QSTR(MP_QSTR_FHD), MP_ROM_INT(FRAMESIZE_FHD) }, - { MP_ROM_QSTR(MP_QSTR_P_HD), MP_ROM_INT(FRAMESIZE_P_HD) }, - { MP_ROM_QSTR(MP_QSTR_P_3MP), MP_ROM_INT(FRAMESIZE_P_3MP) }, - { MP_ROM_QSTR(MP_QSTR_QXGA), MP_ROM_INT(FRAMESIZE_QXGA) }, - { MP_ROM_QSTR(MP_QSTR_QHD), MP_ROM_INT(FRAMESIZE_QHD) }, - { MP_ROM_QSTR(MP_QSTR_WQXGA), MP_ROM_INT(FRAMESIZE_WQXGA) }, - { MP_ROM_QSTR(MP_QSTR_P_FHD), MP_ROM_INT(FRAMESIZE_P_FHD) }, - { MP_ROM_QSTR(MP_QSTR_QSXGA), MP_ROM_INT(FRAMESIZE_QSXGA) }, + { MP_ROM_QSTR(MP_QSTR_R96X96), MP_ROM_INT((mp_uint_t)FRAMESIZE_96X96) }, + { MP_ROM_QSTR(MP_QSTR_QQVGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_QQVGA) }, + { MP_ROM_QSTR(MP_QSTR_R128x128), MP_ROM_INT((mp_uint_t)FRAMESIZE_128X128) }, + { MP_ROM_QSTR(MP_QSTR_QCIF), MP_ROM_INT((mp_uint_t)FRAMESIZE_QCIF) }, + { MP_ROM_QSTR(MP_QSTR_HQVGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_HQVGA) }, + { MP_ROM_QSTR(MP_QSTR_R240X240), MP_ROM_INT((mp_uint_t)FRAMESIZE_240X240) }, + { MP_ROM_QSTR(MP_QSTR_QVGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_QVGA) }, + { MP_ROM_QSTR(MP_QSTR_R320X320), MP_ROM_INT((mp_uint_t)FRAMESIZE_320X320) }, + { MP_ROM_QSTR(MP_QSTR_CIF), MP_ROM_INT((mp_uint_t)FRAMESIZE_CIF) }, + { MP_ROM_QSTR(MP_QSTR_HVGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_HVGA) }, + { MP_ROM_QSTR(MP_QSTR_VGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_VGA) }, + { MP_ROM_QSTR(MP_QSTR_SVGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_SVGA) }, + { MP_ROM_QSTR(MP_QSTR_XGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_XGA) }, + { MP_ROM_QSTR(MP_QSTR_HD), MP_ROM_INT((mp_uint_t)FRAMESIZE_HD) }, + { MP_ROM_QSTR(MP_QSTR_SXGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_SXGA) }, + { MP_ROM_QSTR(MP_QSTR_UXGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_UXGA) }, + { MP_ROM_QSTR(MP_QSTR_FHD), MP_ROM_INT((mp_uint_t)FRAMESIZE_FHD) }, + { MP_ROM_QSTR(MP_QSTR_P_HD), MP_ROM_INT((mp_uint_t)FRAMESIZE_P_HD) }, + { MP_ROM_QSTR(MP_QSTR_P_3MP), MP_ROM_INT((mp_uint_t)FRAMESIZE_P_3MP) }, + { MP_ROM_QSTR(MP_QSTR_QXGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_QXGA) }, + { MP_ROM_QSTR(MP_QSTR_QHD), MP_ROM_INT((mp_uint_t)FRAMESIZE_QHD) }, + { MP_ROM_QSTR(MP_QSTR_WQXGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_WQXGA) }, + { MP_ROM_QSTR(MP_QSTR_P_FHD), MP_ROM_INT((mp_uint_t)FRAMESIZE_P_FHD) }, + { MP_ROM_QSTR(MP_QSTR_QSXGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_QSXGA) }, }; const mp_rom_map_elem_t mp_camera_hal_grab_mode_table[] = { - { MP_ROM_QSTR(MP_QSTR_WHEN_EMPTY), MP_ROM_INT(CAMERA_GRAB_WHEN_EMPTY) }, - { MP_ROM_QSTR(MP_QSTR_LATEST), MP_ROM_INT(CAMERA_GRAB_LATEST) }, + { MP_ROM_QSTR(MP_QSTR_WHEN_EMPTY), MP_ROM_INT((mp_uint_t)CAMERA_GRAB_WHEN_EMPTY) }, + { MP_ROM_QSTR(MP_QSTR_LATEST), MP_ROM_INT((mp_uint_t)CAMERA_GRAB_LATEST) }, }; const mp_rom_map_elem_t mp_camera_hal_gainceiling_table[] = { - { MP_ROM_QSTR(MP_QSTR_2X), MP_ROM_INT(GAINCEILING_2X) }, - { MP_ROM_QSTR(MP_QSTR_4X), MP_ROM_INT(GAINCEILING_4X) }, - { MP_ROM_QSTR(MP_QSTR_8X), MP_ROM_INT(GAINCEILING_8X) }, - { MP_ROM_QSTR(MP_QSTR_16X), MP_ROM_INT(GAINCEILING_16X) }, - { MP_ROM_QSTR(MP_QSTR_32X), MP_ROM_INT(GAINCEILING_32X) }, - { MP_ROM_QSTR(MP_QSTR_64X), MP_ROM_INT(GAINCEILING_64X) }, - { MP_ROM_QSTR(MP_QSTR_128X), MP_ROM_INT(GAINCEILING_128X) }, + { MP_ROM_QSTR(MP_QSTR_2X), MP_ROM_INT((mp_uint_t)GAINCEILING_2X) }, + { MP_ROM_QSTR(MP_QSTR_4X), MP_ROM_INT((mp_uint_t)GAINCEILING_4X) }, + { MP_ROM_QSTR(MP_QSTR_8X), MP_ROM_INT((mp_uint_t)GAINCEILING_8X) }, + { MP_ROM_QSTR(MP_QSTR_16X), MP_ROM_INT((mp_uint_t)GAINCEILING_16X) }, + { MP_ROM_QSTR(MP_QSTR_32X), MP_ROM_INT((mp_uint_t)GAINCEILING_32X) }, + { MP_ROM_QSTR(MP_QSTR_64X), MP_ROM_INT((mp_uint_t)GAINCEILING_64X) }, + { MP_ROM_QSTR(MP_QSTR_128X), MP_ROM_INT((mp_uint_t)GAINCEILING_128X) }, }; -//TODO: Makros with convertion function, since the API will use standarized values. // Helper functions to get and set camera and sensor information -#define SENSOR_STATUS_GETSET_IN_RANGE(type, name, status_field_name, setter_function_name, min_val, max_val) \ - SENSOR_GETSET_IN_RANGE(type, name, status.status_field_name, setter_function_name, min_val, max_val) - #define SENSOR_STATUS_GETSET(type, name, status_field_name, setter_function_name) \ SENSOR_GETSET(type, name, status.status_field_name, setter_function_name) @@ -401,24 +300,16 @@ const mp_rom_map_elem_t mp_camera_hal_gainceiling_table[] = { SENSOR_GET(type, name, field_name) \ SENSOR_SET(type, name, setter_function_name) -#define SENSOR_GETSET_IN_RANGE(type, name, field_name, setter_function_name, min_val, max_val) \ - SENSOR_GET(type, name, field_name) \ - SENSOR_SET_IN_RANGE(type, name, setter_function_name, min_val, max_val) - #define SENSOR_GET(type, name, status_field_name) \ type mp_camera_hal_get_##name(mp_camera_obj_t * self) { \ - if (!self->initialized) { \ - mp_raise_ValueError(MP_ERROR_TEXT("Camera not initialized")); \ - } \ + check_init(self); \ sensor_t *sensor = esp_camera_sensor_get(); \ return sensor->status_field_name; \ } #define SENSOR_SET(type, name, setter_function_name) \ void mp_camera_hal_set_##name(mp_camera_obj_t * self, type value) { \ - if (!self->initialized) { \ - mp_raise_ValueError(MP_ERROR_TEXT("Camera not initialized")); \ - } \ + check_init(self); \ sensor_t *sensor = esp_camera_sensor_get(); \ if (!sensor->setter_function_name) { \ mp_raise_ValueError(MP_ERROR_TEXT("No attribute " #name)); \ @@ -428,28 +319,11 @@ const mp_rom_map_elem_t mp_camera_hal_gainceiling_table[] = { } \ } -#define SENSOR_SET_IN_RANGE(type, name, setter_function_name, min_val, max_val) \ - void mp_camera_hal_set_##name(mp_camera_obj_t * self, type value) { \ - sensor_t *sensor = esp_camera_sensor_get(); \ - if (!self->initialized) { \ - mp_raise_ValueError(MP_ERROR_TEXT("Camera not initialized")); \ - } \ - if (value < min_val || value > max_val) { \ - mp_raise_ValueError(MP_ERROR_TEXT(#name " value must be between " #min_val " and " #max_val)); \ - } \ - if (!sensor->setter_function_name) { \ - mp_raise_ValueError(MP_ERROR_TEXT("No attribute " #name)); \ - } \ - if (sensor->setter_function_name(sensor, value) < 0) { \ - mp_raise_ValueError(MP_ERROR_TEXT("Invalid setting for " #name)); \ - } \ - } - SENSOR_GET(framesize_t, frame_size, status.framesize); -SENSOR_STATUS_GETSET_IN_RANGE(int, contrast, contrast, set_contrast, -2, 2); -SENSOR_STATUS_GETSET_IN_RANGE(int, brightness, brightness, set_brightness, -2, 2); -SENSOR_STATUS_GETSET_IN_RANGE(int, saturation, saturation, set_saturation, -2, 2); -SENSOR_STATUS_GETSET_IN_RANGE(int, sharpness, sharpness, set_sharpness, -2, 2); +SENSOR_STATUS_GETSET(int, contrast, contrast, set_contrast); +SENSOR_STATUS_GETSET(int, brightness, brightness, set_brightness); +SENSOR_STATUS_GETSET(int, saturation, saturation, set_saturation); +SENSOR_STATUS_GETSET(int, sharpness, sharpness, set_sharpness); SENSOR_STATUS_GETSET(int, denoise, denoise, set_denoise); SENSOR_STATUS_GETSET(mp_camera_gainceiling_t, gainceiling, gainceiling, set_gainceiling); SENSOR_STATUS_GETSET(bool, colorbar, colorbar, set_colorbar); @@ -460,11 +334,11 @@ SENSOR_STATUS_GETSET(bool, hmirror, hmirror, set_hmirror); SENSOR_STATUS_GETSET(bool, vflip, vflip, set_vflip); SENSOR_STATUS_GETSET(bool, aec2, aec2, set_aec2); SENSOR_STATUS_GETSET(bool, awb_gain, awb_gain, set_awb_gain); -SENSOR_STATUS_GETSET(int, agc_gain, agc_gain, set_agc_gain); //in_Range not needed since driver limits value -SENSOR_STATUS_GETSET(int, aec_value, aec_value, set_aec_value); //in_Range not needed since driver limits value -SENSOR_STATUS_GETSET_IN_RANGE(int, special_effect, special_effect, set_special_effect, 0, 6); -SENSOR_STATUS_GETSET_IN_RANGE(int, wb_mode, wb_mode, set_wb_mode, 0, 4); -SENSOR_STATUS_GETSET_IN_RANGE(int, ae_level, ae_level, set_ae_level, -2, 2); +SENSOR_STATUS_GETSET(int, agc_gain, agc_gain, set_agc_gain); +SENSOR_STATUS_GETSET(int, aec_value, aec_value, set_aec_value); +SENSOR_STATUS_GETSET(int, special_effect, special_effect, set_special_effect); +SENSOR_STATUS_GETSET(int, wb_mode, wb_mode, set_wb_mode); +SENSOR_STATUS_GETSET(int, ae_level, ae_level, set_ae_level); SENSOR_STATUS_GETSET(bool, dcw, dcw, set_dcw); SENSOR_STATUS_GETSET(bool, bpc, bpc, set_bpc); SENSOR_STATUS_GETSET(bool, wpc, wpc, set_wpc); @@ -472,10 +346,7 @@ SENSOR_STATUS_GETSET(bool, raw_gma, raw_gma, set_raw_gma); SENSOR_STATUS_GETSET(bool, lenc, lenc, set_lenc); void mp_camera_hal_set_frame_size(mp_camera_obj_t * self, framesize_t value) { - if (!self->initialized) { - mp_raise_ValueError(MP_ERROR_TEXT("Camera not initialized")); - } - + check_init(self); sensor_t *sensor = esp_camera_sensor_get(); if (!sensor->set_framesize) { mp_raise_ValueError(MP_ERROR_TEXT("No attribute frame_size")); @@ -494,16 +365,12 @@ void mp_camera_hal_set_frame_size(mp_camera_obj_t * self, framesize_t value) { } int mp_camera_hal_get_quality(mp_camera_obj_t * self) { - if (!self->initialized) { - mp_raise_ValueError(MP_ERROR_TEXT("Camera not initialized")); - } + check_init(self); return self->camera_config.jpeg_quality; } void mp_camera_hal_set_quality(mp_camera_obj_t * self, int value) { - if (!self->initialized) { - mp_raise_ValueError(MP_ERROR_TEXT("Camera not initialized")); - } + check_init(self); sensor_t *sensor = esp_camera_sensor_get(); if (!sensor->set_quality) { mp_raise_ValueError(MP_ERROR_TEXT("No attribute quality")); @@ -528,36 +395,42 @@ int mp_camera_hal_get_fb_count(mp_camera_obj_t *self) { } const char *mp_camera_hal_get_sensor_name(mp_camera_obj_t *self) { + check_init(self); sensor_t *sensor = esp_camera_sensor_get(); camera_sensor_info_t *sensor_info = esp_camera_sensor_get_info(&sensor->id); return sensor_info->name; } bool mp_camera_hal_get_supports_jpeg(mp_camera_obj_t *self) { + check_init(self); sensor_t *sensor = esp_camera_sensor_get(); camera_sensor_info_t *sensor_info = esp_camera_sensor_get_info(&sensor->id); return sensor_info->support_jpeg; } mp_camera_framesize_t mp_camera_hal_get_max_frame_size(mp_camera_obj_t *self) { + check_init(self); sensor_t *sensor = esp_camera_sensor_get(); camera_sensor_info_t *sensor_info = esp_camera_sensor_get_info(&sensor->id); return sensor_info->max_size; } int mp_camera_hal_get_address(mp_camera_obj_t *self) { + check_init(self); sensor_t *sensor = esp_camera_sensor_get(); camera_sensor_info_t *sensor_info = esp_camera_sensor_get_info(&sensor->id); return sensor_info->sccb_addr; } int mp_camera_hal_get_pixel_width(mp_camera_obj_t *self) { + check_init(self); sensor_t *sensor = esp_camera_sensor_get(); framesize_t framesize = sensor->status.framesize; return resolution[framesize].width; } int mp_camera_hal_get_pixel_height(mp_camera_obj_t *self) { + check_init(self); sensor_t *sensor = esp_camera_sensor_get(); framesize_t framesize = sensor->status.framesize; return resolution[framesize].height; diff --git a/src/modcamera.h b/src/modcamera.h index 1e1305b..a3ce749 100644 --- a/src/modcamera.h +++ b/src/modcamera.h @@ -86,18 +86,8 @@ defined (MICROPY_CAMERA_PIN_PCLK) && defined (MICROPY_CAMERA_PIN_VSYNC) && defin #define MICROPY_CAMERA_JPEG_QUALITY (85) #endif -//Supported Camera sensors -#ifndef CONFIG_OV2640_SUPPORT -#define CONFIG_OV2640_SUPPORT 1 -#endif - -#if !defined(CONFIG_OV5640_SUPPORT) && defined(CONFIG_IDF_TARGET_ESP32S3) -#define CONFIG_OV5640_SUPPORT 1 -#endif - typedef pixformat_t hal_camera_pixformat_t; typedef framesize_t hal_camera_framesize_t; -typedef camera_fb_location_t hal_camera_fb_location_t; typedef camera_grab_mode_t hal_camera_grabmode_t; typedef gainceiling_t hal_camera_gainceiling_t; @@ -106,7 +96,6 @@ typedef struct hal_camera_obj { camera_config_t camera_config; bool initialized; camera_fb_t *captured_buffer; - bool bmp_out; } hal_camera_obj_t; #endif // CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 @@ -117,9 +106,7 @@ typedef hal_camera_pixformat_t mp_camera_pixformat_t; typedef hal_camera_framesize_t mp_camera_framesize_t; typedef hal_camera_grabmode_t mp_camera_grabmode_t; typedef hal_camera_gainceiling_t mp_camera_gainceiling_t; -// typedef hal_camera_fb_location_t mp_camera_fb_location_t; //not used at the moment, but might be used in the future -// TODO: Define how to integrate external time source in constructor (e.g. in ESP is LED-Timer). /** * @brief Constructs the camera hardware abstraction layer. * @details The Port-plattform shall define a default pwm-time source and also frame buffer location (no input) @@ -195,22 +182,36 @@ extern void mp_camera_hal_reconfigure(mp_camera_obj_t *self, mp_camera_framesize * @brief Captures an image and returns it as mp_obj_t (e.g. mp_obj_new_memoryview). * * @param self Pointer to the camera object. - * @param out_format Output pixelformat format. * @return Captured image as micropython object. */ -extern mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self, int8_t out_format); +extern mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self); + +/** + * @brief Returns true, if a frame is available. + * + * @param self Pointer to the camera object. + * @return True, if a frame is available. + */ +extern mp_obj_t mp_camera_hal_frame_available(mp_camera_obj_t *self); + +/** + * @brief Frees the buffer of the camera object. + * + * @param self Pointer to the camera object. + */ +extern void mp_camera_hal_free_buffer(mp_camera_obj_t *self); /** * @brief Table mapping pixel formats API to their corresponding values at HAL. * @details Needs to be defined in the port-specific implementation. */ -extern const mp_rom_map_elem_t mp_camera_hal_pixel_format_table[5]; +extern const mp_rom_map_elem_t mp_camera_hal_pixel_format_table[9]; /** * @brief Table mapping frame sizes API to their corresponding values at HAL. * @details Needs to be defined in the port-specific implementation. */ -extern const mp_rom_map_elem_t mp_camera_hal_frame_size_table[22]; +extern const mp_rom_map_elem_t mp_camera_hal_frame_size_table[24]; /** * @brief Table mapping gainceiling API to their corresponding values at HAL. diff --git a/src/modcamera_api.c b/src/modcamera_api.c index fd52cec..39afa71 100644 --- a/src/modcamera_api.c +++ b/src/modcamera_api.c @@ -40,7 +40,7 @@ const mp_obj_type_t camera_type; //Constructor static mp_obj_t mp_camera_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { - enum { ARG_data_pins, ARG_pixel_clock_pin, ARG_vsync_pin, ARG_href_pin, ARG_sda_pin, ARG_scl_pin, ARG_xclock_pin, ARG_xclock_frequency, ARG_powerdown_pin, ARG_reset_pin, ARG_pixel_format, ARG_frame_size, ARG_jpeg_quality, ARG_fb_count, ARG_grab_mode, ARG_init, ARG_bmp_out, NUM_ARGS }; + enum { ARG_data_pins, ARG_pixel_clock_pin, ARG_vsync_pin, ARG_href_pin, ARG_sda_pin, ARG_scl_pin, ARG_xclock_pin, ARG_xclock_frequency, ARG_powerdown_pin, ARG_reset_pin, ARG_pixel_format, ARG_frame_size, ARG_jpeg_quality, ARG_fb_count, ARG_grab_mode, ARG_init, NUM_ARGS }; static const mp_arg_t allowed_args[] = { #ifdef MICROPY_CAMERA_ALL_REQ_PINS_DEFINED { MP_QSTR_data_pins, MP_ARG_OBJ | MP_ARG_KW_ONLY , { .u_obj = MP_ROM_NONE } }, @@ -68,7 +68,6 @@ static mp_obj_t mp_camera_make_new(const mp_obj_type_t *type, size_t n_args, siz { MP_QSTR_fb_count, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = MICROPY_CAMERA_FB_COUNT } }, { MP_QSTR_grab_mode, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = MICROPY_CAMERA_GRAB_MODE } }, { MP_QSTR_init, MP_ARG_BOOL | MP_ARG_KW_ONLY, { .u_bool = true } }, - { MP_QSTR_bmp_out, MP_ARG_BOOL | MP_ARG_KW_ONLY, { .u_bool = false } }, }; mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; @@ -129,35 +128,45 @@ static mp_obj_t mp_camera_make_new(const mp_obj_type_t *type, size_t n_args, siz mp_camera_obj_t *self = mp_obj_malloc_with_finaliser(mp_camera_obj_t, &camera_type); self->base.type = &camera_type; - self->bmp_out = args[ARG_bmp_out].u_bool; mp_camera_hal_construct(self, data_pins, xclock_pin, pixel_clock_pin, vsync_pin, href_pin, powerdown_pin, reset_pin, sda_pin, scl_pin, xclock_frequency, pixel_format, frame_size, jpeg_quality, fb_count, grab_mode); mp_camera_hal_init(self); - - if (mp_camera_hal_capture(self, -1) == mp_const_none){ + if (mp_camera_hal_capture(self) == mp_const_none){ mp_camera_hal_deinit(self); mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("Failed to capture initial frame. Construct a new object with appropriate configuration.")); - return MP_OBJ_FROM_PTR(self); } else { if ( !args[ARG_init].u_bool ){ mp_camera_hal_deinit(self); + } else { + mp_camera_hal_free_buffer(self); } return MP_OBJ_FROM_PTR(self); } -} +} // camera_construct // Main methods -static mp_obj_t camera_capture(size_t n_args, const mp_obj_t *args){ - mp_camera_obj_t *self = MP_OBJ_TO_PTR(args[0]); - int8_t out_format = n_args < 2 ? -1 : mp_obj_get_int(args[1]); - return mp_camera_hal_capture(self, out_format); +static mp_obj_t camera_capture(mp_obj_t self_in){ + mp_camera_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_camera_hal_capture(self); +} +static MP_DEFINE_CONST_FUN_OBJ_1(camera_capture_obj, camera_capture); + +static mp_obj_t camera_frame_available(mp_obj_t self_in){ + mp_camera_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_camera_hal_frame_available(self); +} +static MP_DEFINE_CONST_FUN_OBJ_1(camera_frame_available_obj, camera_frame_available); + +static mp_obj_t camera_free_buf(mp_obj_t self_in) { + mp_camera_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_camera_hal_free_buffer(self); + return mp_const_none; } -static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(camera_capture_obj, 1, 2, camera_capture); +static MP_DEFINE_CONST_FUN_OBJ_1(camera_free_buf_obj, camera_free_buf); static mp_obj_t camera_reconfigure(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args){ - //OPEN: Validate inputs mp_camera_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); enum { ARG_frame_size, ARG_pixel_format, ARG_grab_mode, ARG_fb_count }; static const mp_arg_t allowed_args[] = { @@ -182,7 +191,7 @@ static mp_obj_t camera_reconfigure(size_t n_args, const mp_obj_t *pos_args, mp_m args[ARG_grab_mode].u_obj != MP_ROM_NONE ? args[ARG_grab_mode].u_int : mp_camera_hal_get_grab_mode(self); - uint8_t fb_count = + mp_int_t fb_count = args[ARG_fb_count].u_obj != MP_ROM_NONE ? args[ARG_fb_count].u_int : mp_camera_hal_get_fb_count(self); @@ -206,19 +215,6 @@ static mp_obj_t mp_camera_deinit(mp_obj_t self_in) { } static MP_DEFINE_CONST_FUN_OBJ_1(mp_camera_deinit_obj, mp_camera_deinit); -static mp_obj_t camera_get_bmp_out(mp_obj_t self_in) { - mp_camera_obj_t *self = MP_OBJ_TO_PTR(self_in); - return mp_obj_new_bool(self->bmp_out); -} -static MP_DEFINE_CONST_FUN_OBJ_1(camera_get_bmp_out_obj, camera_get_bmp_out); - -static mp_obj_t camera_set_bmp_out(mp_obj_t self_in, mp_obj_t arg) { - mp_camera_obj_t *self = MP_OBJ_TO_PTR(self_in); - self->bmp_out = mp_obj_is_true(arg); - return mp_const_none; -} -static MP_DEFINE_CONST_FUN_OBJ_2(camera_set_bmp_out_obj, camera_set_bmp_out); - // Destructor static mp_obj_t mp_camera_obj___exit__(size_t n_args, const mp_obj_t *args) { (void)n_args; @@ -293,6 +289,8 @@ CREATE_GETSET_FUNCTIONS(lenc, mp_obj_new_bool, mp_obj_is_true); static const mp_rom_map_elem_t camera_camera_locals_table[] = { { MP_ROM_QSTR(MP_QSTR_reconfigure), MP_ROM_PTR(&camera_reconfigure_obj) }, { MP_ROM_QSTR(MP_QSTR_capture), MP_ROM_PTR(&camera_capture_obj) }, + { MP_ROM_QSTR(MP_QSTR_frame_available), MP_ROM_PTR(&camera_frame_available_obj) }, + { MP_ROM_QSTR(MP_QSTR_free_buffer), MP_ROM_PTR(&camera_free_buf_obj) }, { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&camera_init_obj) }, { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&mp_camera_deinit_obj) }, { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mp_camera_deinit_obj) }, @@ -331,7 +329,6 @@ static const mp_rom_map_elem_t camera_camera_locals_table[] = { ADD_PROPERTY_TO_TABLE(wpc), ADD_PROPERTY_TO_TABLE(raw_gma), ADD_PROPERTY_TO_TABLE(lenc), - ADD_PROPERTY_TO_TABLE(bmp_out), }; static MP_DEFINE_CONST_DICT(camera_camera_locals_dict, camera_camera_locals_table); @@ -366,6 +363,13 @@ MP_CREATE_CONST_TYPE(GainCeiling, mp_camera_gainceiling); static MP_DEFINE_CONST_DICT(mp_camera_grab_mode_locals_dict,mp_camera_hal_grab_mode_table); MP_CREATE_CONST_TYPE(GrabMode, mp_camera_grab_mode); +#ifdef MP_CAMERA_DRIVER_VERSION + static mp_obj_t mp_camera_driver_version(void) { + return mp_obj_new_str(MP_CAMERA_DRIVER_VERSION, strlen(MP_CAMERA_DRIVER_VERSION)); + } + static MP_DEFINE_CONST_FUN_OBJ_0(mp_camera_driver_version_obj, mp_camera_driver_version); +#endif + static const mp_rom_map_elem_t camera_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_camera) }, { MP_ROM_QSTR(MP_QSTR_Camera), MP_ROM_PTR(&camera_type) }, @@ -373,6 +377,9 @@ static const mp_rom_map_elem_t camera_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_FrameSize), MP_ROM_PTR(&mp_camera_frame_size_type) }, { MP_ROM_QSTR(MP_QSTR_GainCeiling), MP_ROM_PTR(&mp_camera_gainceiling_type) }, { MP_ROM_QSTR(MP_QSTR_GrabMode), MP_ROM_PTR(&mp_camera_grab_mode_type) }, + #ifdef MP_CAMERA_DRIVER_VERSION + { MP_ROM_QSTR(MP_QSTR_Version), MP_ROM_PTR(&mp_camera_driver_version_obj) }, + #endif }; static MP_DEFINE_CONST_DICT(camera_module_globals, camera_module_globals_table); diff --git a/tests/esp32_test.py b/tests/esp32_test.py index 23949fa..e29c79f 100644 --- a/tests/esp32_test.py +++ b/tests/esp32_test.py @@ -1,27 +1,82 @@ +import time from camera import Camera, FrameSize, PixelFormat def test_property_get_frame_size(): - cam = Camera() - Frame_Size = FrameSize.VGA - cam.reconfigure(frame_size=Frame_Size.VGA) - assert cam.get_frame_size == Frame_Size - assert cam.get_pixel_width == 640 - assert cam.get_pixel_height == 480 + with Camera() as cam: + print("Test frame size") + Frame_Size = FrameSize.VGA + cam.reconfigure(frame_size=Frame_Size) + assert cam.get_frame_size() == Frame_Size + assert cam.get_pixel_width() == 640 + assert cam.get_pixel_height() == 480 def test_property_get_pixel_format(): - cam = Camera() - Pixel_Format = PixelFormat.RGB565 - cam.reconfigure(pixel_format=PixelFormat.RGB) - assert cam.get_pixel_format == Pixel_Format + with Camera() as cam: + print("Test pixel format") + for Pixel_Format_Name in dir(PixelFormat): + Pixel_Format = getattr(PixelFormat, Pixel_Format_Name) + try: + if Pixel_Format_Name.startswith("_") or Pixel_Format_Name.startswith("RGB888"): + continue + cam.reconfigure(pixel_format=Pixel_Format) + assert cam.get_pixel_format() == Pixel_Format + except Exception: + print("\tFailed test for pixel format", Pixel_Format) +def test_must_be_initialized(): + with Camera(init=False) as cam: + print(f"Testing capture without initalization") + try: + cam.capture() + assert False, "Capture should have failed" + except Exception as e: + if e == "Camera not initialized": + assert False, "Capture should have failed" + else: + assert True + def test_camera_properties(): - cam = Camera() - for name in dir(cam): - if name.startswith('get_'): - prop_name = name[4:] - set_method_name = f'set_{prop_name}' - if hasattr(cam, set_method_name): - set_method = getattr(cam, set_method_name) - get_method = getattr(cam, name) - set_method(1) - assert get_method() == 1, f"Failed for property {prop_name}" \ No newline at end of file + with Camera() as cam: + print(f"Testing get/set methods") + for name in dir(cam): + if name.startswith('get_'): + prop_name = name[4:] + set_method_name = f'set_{prop_name}' + if hasattr(cam, set_method_name): + set_method = getattr(cam, set_method_name) + get_method = getattr(cam, name) + try: + set_method(1) + assert get_method() == 1 + except Exception: + print("\tFailed test for property", prop_name) + +def test_invalid_settings(): + print(f"Testing invalid settings") + invalid_settings = [ + {"xclk_freq": 21000000}, + {"frame_size": 23}, + {"pixel_format": 7}, + {"jpeg_quality": 101}, + {"jpeg_quality": -1}, + {"grab_mode": 3}, + ] + Delay= 10 + + for settings in invalid_settings: + param, invalid_value = next(iter(settings.items())) + try: + print("testing",param,"=",invalid_value) + time.sleep_ms(Delay) + with Camera(**{param: invalid_value}) as cam: + print(f"\tFailed test for {param} with value {invalid_value}") + except Exception as e: + time.sleep_ms(Delay) + +if __name__ == "__main__": + test_property_get_frame_size() + test_property_get_pixel_format() + test_must_be_initialized() + test_camera_properties() + test_invalid_settings() + diff --git a/typings/acamera.pyi b/typings/acamera.pyi new file mode 100644 index 0000000..88ede9f --- /dev/null +++ b/typings/acamera.pyi @@ -0,0 +1,9 @@ +from camera import Camera as _Camera + +class Camera(_Camera): + async def acapture(self) -> memoryview: + """Asynchronously capture a frame and return it as a memoryview. + + Yields control back to the event loop while waiting for a frame to become available. + """ + ... \ No newline at end of file diff --git a/typings/camera.pyi b/typings/camera.pyi new file mode 100644 index 0000000..679d9be --- /dev/null +++ b/typings/camera.pyi @@ -0,0 +1,348 @@ +from __future__ import annotations +from typing import Final + +class GainCeiling(): + X2: Final[int] = 0 # 2X gain + X4: Final[int] = 1 # 4X gain + X8: Final[int] = 2 # 8X gain + X16: Final[int] = 3 # 16X gain + X32: Final[int] = 4 # 32X gain + X64: Final[int] = 5 # 64X gain + X128: Final[int] = 6 # 128X gain + def __init__(self, *argv, **kwargs) -> None: + ... + + +class GrabMode(): + WHEN_EMPTY: Final[int] = 0 + LATEST: Final[int] = 1 + def __init__(self, *argv, **kwargs) -> None: + ... + + +class PixelFormat(): + RGB555: Final[int] = 8 + RGB565: Final[int] = 0 + RGB888: Final[int] = 5 + YUV420: Final[int] = 2 + YUV422: Final[int] = 1 + RGB444: Final[int] = 7 + GRAYSCALE: Final[int] = 3 + JPEG: Final[int] = 4 + RAW: Final[int] = 6 + def __init__(self, *argv, **kwargs) -> None: + ... + + +class FrameSize(): + R240X240: Final[int] = 5 + XGA: Final[int] = 12 + R320X320: Final[int] = 7 + QVGA: Final[int] = 6 + R128x128: int = 2 + QXGA: Final[int] = 19 + VGA: Final[int] = 10 + R96X96: Final[int] = 0 + WQXGA: Final[int] = 21 + SVGA: Final[int] = 11 + UXGA: Final[int] = 15 + SXGA: Final[int] = 14 + HQVGA: Final[int] = 4 + QSXGA: Final[int] = 23 + HVGA: Final[int] = 9 + CIF: Final[int] = 8 + HD: Final[int] = 13 + FHD: Final[int] = 16 + QHD: Final[int] = 20 + P_3MP: Final[int] = 18 + QQVGA: Final[int] = 1 + P_FHD: Final[int] = 22 + QCIF: Final[int] = 3 + P_HD: Final[int] = 17 + def __init__(self, *argv, **kwargs) -> None: + ... + + +class Camera(): + def __init__(self, *, + data_pins: list[int] | None = None, + pclk_pin: int | None = None, + vsync_pin: int | None = None, + href_pin: int | None = None, + sda_pin: int | None = None, + scl_pin: int | None = None, + xclk_pin: int | None = None, + xclk_freq: int = 20000000, + powerdown_pin: int = -1, + reset_pin: int = -1, + pixel_format: int = PixelFormat.RGB565, + frame_size: int = FrameSize.QVGA, + jpeg_quality: int = 85, + fb_count: int = 1, + grab_mode: int = GrabMode.WHEN_EMPTY, + init: bool = True) -> None: + ... + + def get_special_effect(self) -> int: + """Get the current special effect setting.""" + ... + + def set_special_effect(self, value: int) -> None: + """Set the special effect.""" + ... + + def set_awb_gain(self, value: bool) -> None: + """Enable/disable Auto White Balance Gain.""" + ... + + def get_awb_gain(self) -> bool: + """Get Auto White Balance Gain state.""" + ... + + def set_agc_gain(self, value: int) -> None: + """Set the Auto Gain Control gain value.""" + ... + + def get_agc_gain(self) -> int: + """Get the Auto Gain Control gain value.""" + ... + + def set_aec_value(self, value: int) -> None: + """Set the Auto Exposure Control value.""" + ... + + def get_aec_value(self) -> int: + """Get the Auto Exposure Control value.""" + ... + + def set_bpc(self, value: bool) -> None: + """Enable/disable Bad Pixel Correction.""" + ... + + def get_bpc(self) -> bool: + """Get Bad Pixel Correction state.""" + ... + + def set_contrast(self, value: int) -> None: + """Set the contrast level (-2 to 2).""" + ... + + def get_contrast(self) -> int: + """Get the contrast level.""" + ... + + def set_colorbar(self, value: bool) -> None: + """Enable/disable color bar test pattern.""" + ... + + def get_colorbar(self) -> bool: + """Get color bar test pattern state.""" + ... + + def set_brightness(self, value: int) -> None: + """Set the brightness level (-2 to 2).""" + ... + + def get_brightness(self) -> int: + """Get the brightness level.""" + ... + + def set_aec2(self, value: bool) -> None: + """Enable/disable AEC DSP.""" + ... + + def get_aec2(self) -> bool: + """Get AEC DSP state.""" + ... + + def set_whitebal(self, value: bool) -> None: + """Enable/disable white balance.""" + ... + + def get_whitebal(self) -> bool: + """Get white balance state.""" + ... + + def set_wb_mode(self, value: int) -> None: + """Set white balance mode.""" + ... + + def get_wb_mode(self) -> int: + """Get white balance mode.""" + ... + + def set_vflip(self, value: bool) -> None: + """Enable/disable vertical flip.""" + ... + + def get_vflip(self) -> bool: + """Get vertical flip state.""" + ... + + def set_wpc(self, value: bool) -> None: + """Enable/disable White Pixel Correction.""" + ... + + def get_wpc(self) -> bool: + """Get White Pixel Correction state.""" + ... + + def set_ae_level(self, value: int) -> None: + """Set Auto Exposure level (-2 to 2).""" + ... + + def get_ae_level(self) -> int: + """Get Auto Exposure level.""" + ... + + def reconfigure(self, *, frame_size: int | None = None, + pixel_format: int | None = None, + grab_mode: int | None = None, + fb_count: int | None = None) -> None: + """Reconfigure camera with new settings.""" + ... + + def init(self) -> None: + """Initialize the camera.""" + ... + + def deinit(self) -> None: + """Deinitialize the camera.""" + ... + + def set_dcw(self, value: bool) -> None: + """Enable/disable DCW (Downsize EN).""" + ... + + def get_dcw(self) -> bool: + """Get DCW (Downsize EN) state.""" + ... + + def set_sharpness(self, value: int) -> None: + """Set the sharpness level (-2 to 2).""" + ... + + def get_sharpness(self) -> int: + """Get the sharpness level.""" + ... + + def set_saturation(self, value: int) -> None: + """Set the saturation level (-2 to 2).""" + ... + + def get_saturation(self) -> int: + """Get the saturation level.""" + ... + + def set_raw_gma(self, value: bool) -> None: + """Enable/disable GMA (Gamma) Correction.""" + ... + + def get_raw_gma(self) -> bool: + """Get GMA (Gamma) Correction state.""" + ... + + def set_quality(self, value: int) -> None: + """Set JPEG quality (0-63).""" + ... + + def get_quality(self) -> int: + """Get JPEG quality.""" + ... + + def set_frame_size(self, value: int) -> None: + """Set frame size.""" + ... + + def get_frame_size(self) -> int: + """Get frame size.""" + ... + + def set_exposure_ctrl(self, value: bool) -> None: + """Enable/disable Auto Exposure Control.""" + ... + + def get_exposure_ctrl(self) -> bool: + """Get Auto Exposure Control state.""" + ... + + def set_denoise(self, value: int) -> None: + """Set denoise level.""" + ... + + def get_denoise(self) -> int: + """Get denoise level.""" + ... + + def set_gain_ctrl(self, value: bool) -> None: + """Enable/disable Auto Gain Control.""" + ... + + def get_gain_ctrl(self) -> bool: + """Get Auto Gain Control state.""" + ... + + def set_lenc(self, value: bool) -> None: + """Enable/disable Lens Correction.""" + ... + + def get_lenc(self) -> bool: + """Get Lens Correction state.""" + ... + + def set_hmirror(self, value: bool) -> None: + """Enable/disable horizontal mirror.""" + ... + + def get_hmirror(self) -> bool: + """Get horizontal mirror state.""" + ... + + def set_gainceiling(self, value: int) -> None: + """Set gain ceiling.""" + ... + + def get_gainceiling(self) -> int: + """Get gain ceiling.""" + ... + + def frame_available(self) -> bool: + """Check if a frame is available.""" + ... + + def capture(self) -> memoryview: + """Capture a frame and return it as a memoryview.""" + ... + + def free_buffer(self) -> None: + """Free the frame buffer.""" + ... + + def get_pixel_width(self) -> int: + """Get frame width in pixels.""" + ... + + def get_pixel_height(self) -> int: + """Get frame height in pixels.""" + ... + + def get_pixel_format(self) -> int: + """Get current pixel format.""" + ... + + def get_sensor_name(self) -> str: + """Get camera sensor name.""" + ... + + def get_max_frame_size(self) -> int: + """Get maximum supported frame size.""" + ... + + def get_fb_count(self) -> int: + """Get frame buffer count.""" + ... + + def get_grab_mode(self) -> int: + """Get current grab mode.""" + ... +