diff --git a/.github/workflows/ESP32.yml b/.github/workflows/ESP32.yml index 7314a49..e124903 100644 --- a/.github/workflows/ESP32.yml +++ b/.github/workflows/ESP32.yml @@ -7,7 +7,7 @@ on: - 'src/**' - '.github/workflows/*.yml' tags-ignore: - - '**' + - 'v*' pull_request: branches: - master diff --git a/README.md b/README.md index 4b6593b..8fc9b9f 100644 --- a/README.md +++ b/README.md @@ -99,14 +99,31 @@ cam.init() ### Capture image ```python -img = cam.capture() #capture image as configured -img_rgb888 = cam.capture(PixelFormat.RGB888) #capture image and convert it to RGB888 +img = cam.capture() ``` Arguments for capture - out_format: Output format as PixelFormat (optional) +### Convert image to another format + +You can either convert the image with the `capture` method directly passing the desired output format: +```python +img_rgb888 = cam.capture(PixelFormat.RGB888) #capture image as configured (e.g. JPEG), convert it to RGB888 and return the converted image +``` +Or you can first capture the image and then convert it to the desired PixelFormat with the `convert` method. +Doing so you can have both, the captured and the converted image. Note that more memory will be used. +```python +img = cam.capture() +img_rgb888 = cam.convert(PixelFormat.RGB888) #converts the last captured image to RGB888 and returns the converted image +``` + +Convertion supported +- from JPEG to RGB565 +- to RGB888 in general +- to JPEG in gerenal (use the `set_quality` method to set the desired JPEG quality) + ### Camera reconfiguration ```python @@ -217,12 +234,12 @@ 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 @@ -253,7 +270,7 @@ 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, fb_count = 1, jpeg_quality=85%). +I didn't use a calibrated osziloscope, but here is a benchmark with my ESP32S3 (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 aplly for other PixelFormats. | Frame Size | GRAYSCALE | RGB565 | YUV422 | JPEG | JPEG -> RGB565 | JPEG -> RGB888 | JPEG (fb=2) | @@ -263,8 +280,8 @@ Using fb_count=2 theoretically can double the FPS (see JPEG with fb_count=2). Th | 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 | +| QVGA | 12 | 11 | 12 | 25 | 25 | 25 | 50 | +| CIF | 12.5 | No img | No img | 6.3 | 8.3 | 8.3 | 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 | diff --git a/src/modcamera.c b/src/modcamera.c index fd3c008..ade8386 100644 --- a/src/modcamera.c +++ b/src/modcamera.c @@ -148,11 +148,15 @@ void mp_camera_hal_construct( // 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; self->captured_buffer = NULL; + self->converted_buffer.len = 0; + self->converted_buffer.buf = NULL; + self->converted_buffer.typecode = 'B'; + self->bmp_out = false; } void mp_camera_hal_init(mp_camera_obj_t *self) { @@ -171,6 +175,11 @@ void mp_camera_hal_init(mp_camera_obj_t *self) { void mp_camera_hal_deinit(mp_camera_obj_t *self) { if (self->initialized) { + if (self->converted_buffer.buf) { + free(self->converted_buffer.buf); + self->converted_buffer.buf = NULL; + self->converted_buffer.len = 0; + } if (self->captured_buffer) { esp_camera_return_all(); self->captured_buffer = NULL; @@ -205,6 +214,61 @@ void mp_camera_hal_reconfigure(mp_camera_obj_t *self, mp_camera_framesize_t fram ESP_LOGI(TAG, "Camera reconfigured successfully"); } +static bool ensure_buffer(mp_camera_obj_t *self, size_t req_len) { + if (self->converted_buffer.len == req_len) { + return true; + } + if (self->converted_buffer.len > 0 || self->converted_buffer.buf) { + free(self->converted_buffer.buf); + } + self->converted_buffer.buf = (uint8_t *)malloc(req_len); + if (!self->converted_buffer.buf) { + self->converted_buffer.len = 0; + ESP_LOGE(TAG, "converted_buffer malloc failed"); + return false; + } + self->converted_buffer.len = req_len; + return true; +} + +static bool mp_camera_convert(mp_camera_obj_t *self, mp_camera_pixformat_t out_format) { + ESP_LOGI(TAG, "Converting image to pixel format: %d", out_format); + + switch (out_format) { + case PIXFORMAT_JPEG: + return frame2jpg(self->captured_buffer, self->camera_config.jpeg_quality, self->converted_buffer.buf, &self->converted_buffer.len); + + case PIXFORMAT_RGB888: + if (ensure_buffer(self, self->captured_buffer->width * self->captured_buffer->height * 3)) { + return fmt2rgb888(self->captured_buffer->buf, self->captured_buffer->len, self->captured_buffer->format, self->converted_buffer.buf); + } else { + return false; + } + + case PIXFORMAT_RGB565: + if (self->camera_config.pixel_format == PIXFORMAT_JPEG) { + if (ensure_buffer(self, self->captured_buffer->width * self->captured_buffer->height * 2)) { + return jpg2rgb565(self->captured_buffer->buf, self->captured_buffer->len, self->converted_buffer.buf, JPG_SCALE_NONE); + } else { + return false; + } + } else { + mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("Can only convert JPEG to RGB565")); + } + + default: + mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("Unsupported pixel format for conversion")); + } +} +mp_obj_t mp_camera_hal_convert(mp_camera_obj_t *self, int8_t out_format) { + check_init(self); + if (mp_camera_convert(self, (mp_camera_pixformat_t)out_format)) { + return mp_obj_new_memoryview('b', self->converted_buffer.len, self->converted_buffer.buf); + } else { + return mp_const_none; + } +} + mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self, int8_t out_format) { check_init(self); if (self->captured_buffer) { @@ -212,14 +276,6 @@ mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self, int8_t out_format) { 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) { @@ -228,56 +284,12 @@ 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)) { - 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: - 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); - 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; - + if (mp_camera_convert(self, (mp_camera_pixformat_t)out_format)) { + esp_camera_fb_return(self->captured_buffer); + mp_obj_t result = mp_obj_new_memoryview('b', self->converted_buffer.len, self->converted_buffer.buf); + return result; + } else { + return mp_const_none; } } @@ -286,16 +298,15 @@ mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self, int8_t out_format) { 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)) { + if (frame2bmp(self->captured_buffer, self->converted_buffer.buf, &self->converted_buffer.len)) { esp_camera_fb_return(self->captured_buffer); - mp_obj_t result = mp_obj_new_memoryview('b', out_len, out_buf); + mp_obj_t result = mp_obj_new_memoryview('b', self->converted_buffer.len, self->converted_buffer.buf); return result; } else { - free(out_buf); - out_buf = NULL; - out_len = 0; + free(self->converted_buffer.buf); + self->converted_buffer.buf = NULL; + self->converted_buffer.len = 0; mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("Failed to convert image to BMP")); - return mp_const_none; } } } // mp_camera_hal_capture diff --git a/src/modcamera.h b/src/modcamera.h index 1c36751..7e68be8 100644 --- a/src/modcamera.h +++ b/src/modcamera.h @@ -98,6 +98,7 @@ typedef struct hal_camera_obj { bool initialized; camera_fb_t *captured_buffer; bool bmp_out; + mp_buffer_info_t converted_buffer; } hal_camera_obj_t; #endif // CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 @@ -191,6 +192,15 @@ extern void mp_camera_hal_reconfigure(mp_camera_obj_t *self, mp_camera_framesize */ extern mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self, int8_t out_format); +/** + * @brief Converts an image from one pixelformat to another. + * + * @param self Pointer to the camera object. + * @param out_format Output pixelformat format. + * @return Converted image as micropython object. + */ +extern mp_obj_t mp_camera_hal_convert(mp_camera_obj_t *self, int8_t out_format); + /** * @brief Table mapping pixel formats API to their corresponding values at HAL. * @details Needs to be defined in the port-specific implementation. diff --git a/src/modcamera_api.c b/src/modcamera_api.c index 71cc2b0..0ea8e9d 100644 --- a/src/modcamera_api.c +++ b/src/modcamera_api.c @@ -154,6 +154,13 @@ static mp_obj_t camera_capture(size_t n_args, const mp_obj_t *args){ } static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(camera_capture_obj, 1, 2, camera_capture); +static mp_obj_t camera_convert(mp_obj_t self_in, mp_obj_t arg) { + mp_camera_obj_t *self = MP_OBJ_TO_PTR(self_in); + int8_t out_format = mp_obj_get_int(arg); + return mp_camera_hal_convert(self, out_format); +} +static MP_DEFINE_CONST_FUN_OBJ_2(camera_convert_obj, camera_convert); + 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]); @@ -291,6 +298,7 @@ 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_convert), MP_ROM_PTR(&camera_convert_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) },