diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..d612a9d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +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. diff --git a/.github/ISSUE_TEMPLATE/general-issue.md b/.github/ISSUE_TEMPLATE/general-issue.md new file mode 100644 index 0000000..5460346 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/general-issue.md @@ -0,0 +1,10 @@ +--- +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! diff --git a/.github/workflows/ESP32.yml b/.github/workflows/ESP32.yml index 9548001..65a89cc 100644 --- a/.github/workflows/ESP32.yml +++ b/.github/workflows/ESP32.yml @@ -78,7 +78,9 @@ jobs: cd esp-idf ./install.sh all cd components - git clone https://github.com/cnadler86/esp32-camera + 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 @@ -145,6 +147,8 @@ jobs: # Build MicroPython for each board - name: Build MicroPython run: | + cd ~/esp-idf/components/esp32-camera + CAM_DRIVER=$(git describe --tags --always --dirty) cd ~/micropython/ports/esp32 source ~/esp-idf/export.sh @@ -153,9 +157,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 diff --git a/README.md b/README.md index afba7ee..6df5c3e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![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). +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 FW images and supports a lot of cameras out of the box. 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. @@ -58,21 +58,21 @@ 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) +- powerdown_pin: Powerdown pin +- reset_pin: Reset pin - 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) +- bmp_out: Image captured output converted to bitmap (default: False) **Default values:** @@ -82,7 +82,7 @@ The following keyword arguments have default values: - 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 @@ -134,6 +134,17 @@ 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). Note that each method requires a "get_" or "set_" prefix, depending on the desired action. +To get the version of the camera driver used: + +```python +import camera +vers = camera.Version() +``` + +### Additional information + +The FW 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 FW ### Setting up the build environment (DIY method) @@ -142,20 +153,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 (I used v2.0.15). 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/espressif/esp32-camera.git ``` -Alternatively, you can clone the repository inside the esp-idf/components folder instead of altering the idf_component.yml file. +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 -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 @@ -188,7 +199,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 @@ -214,6 +224,9 @@ Example for Xiao sense: #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 FW 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. ### Build the API @@ -239,28 +252,37 @@ If you experience problems, visit [MicroPython external C modules](https://docs. ## 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 | +I didn't use a calibrated osziloscope, but here is a benchmark with my ESP32S3 (GrabMode=LATEST, fb_count = 1, jpeg_quality=85%). +Using fb_count=2 theoretically can double the FPS (see JPEG with fb_count=2). This might also aplly for other PixelFormats. + +| Frame Size | GRAYSCALE | RGB565 | YUV422 | JPEG | JPEG -> RGB565 | JPEG -> RGB888 | JPEG (fb=2) | +|------------|-----------|--------|--------|--------|----------------|----------------|-------------| +| R96X96 | 12.5 | 12.5 | 12.5 | No img | No img | No img | No img | +| QQVGA | 12.5 | 12.5 | 12.5 | 25 | 25 | 25 | 50 | +| QCIF | 11 | 11 | 11.5 | 25 | 25 | 25 | 50 | +| HQVGA | 12.5 | 12.5 | 12.5 | 25 | 16.7 | 16.7 | 50 | +| R240X240 | 12.5 | 12.5 | 11.5 | 25 | 16.7 | 12.5 | 50 | +| QVGA | 12 | 11 | 12 | 25 | 12.5 | 12.5 | 50 | +| CIF | 12.5 | No img | No img | 6.3 | 1.6 | 1.6 | 12.5 | +| HVGA | 3 | 3 | 2.5 | 12.5 | 6.3 | 6.3 | 25 | +| VGA | 3 | 3 | 3 | 12.5 | 3.6 | 3.6 | 25 | +| SVGA | 3 | 3 | 3 | 12.5 | 2.8 | 2.5 | 25 | +| XGA | No img | No img | No img | 6.3 | 1.6 | 1.6 | 12.5 | +| HD | No img | No img | No img | 6.3 | 1.4 | 1.3 | 12.5 | +| SXGA | 2 | 2 | 2 | 6.3 | 1 | 1 | 12.5 | +| UXGA | No img | No img | No img | 6.3 | 0.7 | 0.7 | 12.5 | + + +Looking at the results: image conversion make only sense for frame sized below QVGA or if capturing the image in the intended pixelformat and frame size combination fails. + +## Troubleshoot + +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) ## 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 diff --git a/examples/CameraSettings.html b/examples/CameraSettings.html index 27e7447..b5fde3d 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,26 +170,29 @@

Micropython Camera Stream

diff --git a/examples/CameraSettings.py b/examples/CameraSettings.py index 26512c5..21f50c4 100644 --- a/examples/CameraSettings.py +++ b/examples/CameraSettings.py @@ -28,7 +28,8 @@ async def stream_camera(writer): 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() 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/micropython.cmake b/src/micropython.cmake index 68c68c8..66fd308 100644 --- a/src/micropython.cmake +++ b/src/micropython.cmake @@ -8,19 +8,28 @@ target_sources(usermod_mp_camera INTERFACE ${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 -) +if(EXISTS "${IDF_PATH}/components/esp32-camera") + 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 + ) +else() + target_include_directories(usermod_mp_camera INTERFACE + ${CMAKE_CURRENT_LIST_DIR}) +endif() if (MICROPY_CAMERA_MODEL) target_compile_definitions(usermod_mp_camera INTERFACE MICROPY_CAMERA_MODEL_${MICROPY_CAMERA_MODEL}=1) endif() +if (MP_CAMERA_DRIVER_VERSION) + target_compile_definitions(usermod_mp_camera INTERFACE MP_CAMERA_DRIVER_VERSION=\"${MP_CAMERA_DRIVER_VERSION}\") +endif() + target_link_libraries(usermod INTERFACE usermod_mp_camera) micropy_gather_target_properties(usermod_mp_camera) \ No newline at end of file diff --git a/src/modcamera.c b/src/modcamera.c index 041554b..fd3c008 100644 --- a/src/modcamera.c +++ b/src/modcamera.c @@ -29,51 +29,15 @@ #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 +49,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 > 20000000) { + mp_raise_ValueError(MP_ERROR_TEXT("xclk frequency cannot be grather than 20MHz")); + } 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 +121,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,23 +137,14 @@ 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; @@ -161,19 +165,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,73 +176,37 @@ 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")); - } + check_init(self); if (self->captured_buffer) { esp_camera_fb_return(self->captured_buffer); self->captured_buffer = NULL; @@ -271,6 +228,7 @@ mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self, int8_t out_format) { } if (out_format >= 0 && (mp_camera_pixformat_t)out_format != self->camera_config.pixel_format) { + ESP_LOGI(TAG, "Converting image to pixel format: %d", out_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)) { @@ -297,6 +255,12 @@ mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self, int8_t out_format) { } case PIXFORMAT_RGB565: + out_len = self->captured_buffer->width * self->captured_buffer->height * 2; + out_buf = (uint8_t *)malloc(out_len); + if (!out_buf) { + ESP_LOGE(TAG, "out_buf malloc failed"); + return mp_const_none; + } 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); @@ -318,7 +282,7 @@ mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self, int8_t out_format) { } if (self->bmp_out == false) { - ESP_LOGI(TAG, "Returning imgae without conversion"); + ESP_LOGI(TAG, "Returning image without conversion"); return mp_obj_new_memoryview('b', self->captured_buffer->len, self->captured_buffer->buf); } else { ESP_LOGI(TAG, "Returning image as bitmap"); @@ -334,7 +298,7 @@ mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self, int8_t out_format) { return mp_const_none; } } -} +} // mp_camera_hal_capture bool mp_camera_hal_initialized(mp_camera_obj_t *self){ return self->initialized; @@ -351,10 +315,12 @@ const mp_rom_map_elem_t mp_camera_hal_pixel_format_table[] = { 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_R128x128), MP_ROM_INT(FRAMESIZE_128X128) }, { 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_R320X320), MP_ROM_INT(FRAMESIZE_320X320) }, { 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) }, @@ -407,18 +373,14 @@ const mp_rom_map_elem_t mp_camera_hal_gainceiling_table[] = { #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)); \ @@ -431,9 +393,7 @@ 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")); \ - } \ + check_init(self); \ if (value < min_val || value > max_val) { \ mp_raise_ValueError(MP_ERROR_TEXT(#name " value must be between " #min_val " and " #max_val)); \ } \ @@ -472,10 +432,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 +451,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 +481,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..1c36751 100644 --- a/src/modcamera.h +++ b/src/modcamera.h @@ -86,15 +86,6 @@ 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; @@ -210,7 +201,7 @@ extern const mp_rom_map_elem_t mp_camera_hal_pixel_format_table[5]; * @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..71cc2b0 100644 --- a/src/modcamera_api.c +++ b/src/modcamera_api.c @@ -135,18 +135,16 @@ static mp_obj_t mp_camera_make_new(const mp_obj_type_t *type, size_t n_args, siz 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){ 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); } 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){ @@ -182,7 +180,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); @@ -366,6 +364,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 +378,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() +