Skip to content

Commit

Permalink
Img conv fun (#21)
Browse files Browse the repository at this point in the history
Adds more functionality and improves the performance in image conversion.
Fix #18
  • Loading branch information
cnadler86 authored Dec 17, 2024
1 parent 86499f6 commit e462cac
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 76 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ESP32.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
- 'src/**'
- '.github/workflows/*.yml'
tags-ignore:
- '**'
- 'v*'
pull_request:
branches:
- master
Expand Down
37 changes: 27 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) |
Expand All @@ -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 |
Expand Down
141 changes: 76 additions & 65 deletions src/modcamera.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -205,21 +214,68 @@ 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) {
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) {
Expand All @@ -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;
}
}

Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions src/modcamera.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions src/modcamera_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -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) },
Expand Down

0 comments on commit e462cac

Please sign in to comment.