mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-01 18:50:34 +03:00
Merge branch 'fix/jpeg_enc_encrypt_v5.5' into 'release/v5.5'
fix(jpeg): Jpeg can encode and decode in encryption situation (backport v5.5) See merge request espressif/esp-idf!50670
This commit is contained in:
@@ -183,7 +183,7 @@ examples/peripherals/isp/multi_pipelines:
|
||||
|
||||
examples/peripherals/jpeg/jpeg_decode:
|
||||
disable:
|
||||
- if: SOC_JPEG_CODEC_SUPPORTED != 1
|
||||
- if: SOC_JPEG_DECODE_SUPPORTED != 1
|
||||
depends_components:
|
||||
- esp_driver_jpeg
|
||||
|
||||
|
||||
@@ -5,4 +5,4 @@ cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(jpeg_decode)
|
||||
project(jpeg_decode_example)
|
||||
|
||||
@@ -5,21 +5,25 @@
|
||||
|
||||
## Overview
|
||||
|
||||
This example demonstrates how to use the JPEG hardware decoder to decode a 1080p and a 720p picture:
|
||||
This example demonstrates how to use the JPEG hardware decoder to decode one embedded JPEG image into `RGB888` raw bytes in default `BGR24` order.
|
||||
|
||||
If you have a bunch of big JPEG picture need to be decoded, such as `*.jpg` -> `*.rgb`, and this example uses hardware JPEG decoder to accelerate the decoding.
|
||||
The example performs:
|
||||
|
||||
## How to use example
|
||||
- Embedding `main/assets/image.jpg` into the application image
|
||||
- Letting the JPEG decoder read the embedded JPEG bitstream directly from flash
|
||||
- Parsing the JPEG header with `jpeg_decoder_get_info()`
|
||||
- Decoding the image into an `RGB888` output buffer with default `BGR24` byte order
|
||||
- Allocating the output buffer for padded dimensions when the JPEG block layout rounds width or height up to 16-pixel boundaries
|
||||
- Base64-encoding the decoded raw pixels and printing them with machine-parseable UART markers
|
||||
- Letting pytest rebuild `jpeg_decode_result.ppm` for inspection and compare it against `golden_output.ppm`
|
||||
|
||||
### Prerequisites Required
|
||||
## Hardware Required
|
||||
|
||||
This example demonstrates the flexibility of decoding pictures by decoding two different sizes: one in 1080p and another in 720p. It showcases how you can easily modify the code to meet your specific requirements, such as only decoding 1080p photos.
|
||||
Any board based on a supported target can be used. No SD card or external storage setup is required.
|
||||
|
||||
### Build and Flash
|
||||
## Build and Flash
|
||||
|
||||
Before you start build and flash this example, please put the image `esp720.jpg` and `esp1080.jpg` in your sdcard.
|
||||
|
||||
Enter `idf.py -p PORT flash monitor` to build, flash and monitor the project.
|
||||
Run `idf.py -p PORT flash monitor` to build, flash and monitor the project.
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
@@ -27,32 +31,53 @@ See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/l
|
||||
|
||||
## Example Output
|
||||
|
||||
```bash
|
||||
I (1116) jpeg.example: Initializing SD card
|
||||
I (1116) gpio: GPIO[43]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
|
||||
I (1126) gpio: GPIO[44]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
|
||||
I (1136) gpio: GPIO[39]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
|
||||
I (1146) gpio: GPIO[40]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
|
||||
I (1156) gpio: GPIO[41]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
|
||||
I (1166) gpio: GPIO[42]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
|
||||
I (1416) gpio: GPIO[42]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
|
||||
Name: SD64G
|
||||
Type: SDHC/SDXC
|
||||
Speed: 40.00 MHz (limit: 40.00 MHz)
|
||||
Size: 60906MB
|
||||
CSD: ver=2, sector_size=512, capacity=124735488 read_bl_len=9
|
||||
SSR: bus_width=4
|
||||
I (1436) jpeg.example: jpg_file_1080:/sdcard/esp1080.jpg
|
||||
I (1696) jpeg.example: jpg_file_1080:/sdcard/esp720.jpg
|
||||
I (1796) jpeg.example: header parsed, width is 1920, height is 1080
|
||||
I (1846) jpeg.example: raw_file_1080:/sdcard/out.rgb
|
||||
I (11836) jpeg.example: raw_file_720:/sdcard/out2.rgb
|
||||
I (13336) jpeg.example: Card unmounted
|
||||
I (13336) main_task: Returned from app_main()
|
||||
```text
|
||||
Loading embedded JPEG from flash...
|
||||
Embedded JPEG size: 43700 bytes
|
||||
JPEG header parsed: width=320 height=240
|
||||
Decoding JPEG -> RGB888...
|
||||
Decoded RGB888 size: 245760 bytes
|
||||
JPEG_DECODE_INFO width=320 height=240 padded_width=320 padded_height=256 format=RGB888 encoding=base64 size=245760
|
||||
JPEG_DECODE_BASE64_BEGIN
|
||||
JPEG_DECODE_BASE64 ...
|
||||
JPEG_DECODE_BASE64 ...
|
||||
JPEG_DECODE_BASE64_END
|
||||
JPEG decode demo done.
|
||||
```
|
||||
|
||||
Also, the helper script [open_raw_picture.py](./open_raw_picture.py) simplifies the visualization of the output on your computer. For this to work, go to `examples/peripheral/jpeg/jpeg_decode` and install the requirements by running `pip install -r requirements.txt`.
|
||||
`padded_width` and `padded_height` report the actual decoded buffer dimensions. For JPEGs whose block layout pads the output to 16-pixel boundaries, these values can be larger than the visible `width` and `height`, so the output buffer must be sized for the padded image.
|
||||
|
||||
## Pytest Regression Check
|
||||
|
||||
The accompanying `pytest_jpeg_decode.py` script waits for the `JPEG_DECODE_INFO` and `JPEG_DECODE_BASE64` markers, reconstructs the decoded raw pixel output, crops away padded rows, and saves the visible image as:
|
||||
|
||||
- `dut.logdir/jpeg_decode_result.ppm`
|
||||
|
||||
The test writes the `PPM` file and compares it with `golden_output.ppm`. This makes the example both a functional regression test and a host-side artifact generator for inspection.
|
||||
|
||||
## Running Pytest Locally And Viewing The Image
|
||||
|
||||
To run the pytest helper locally on hardware, build the example for your target first, then invoke the test script with the target and serial port:
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32p4 build
|
||||
pytest --target esp32p4 --port PORT pytest_jpeg_decode.py
|
||||
```
|
||||
|
||||
Replace `esp32p4` with another supported target such as `esp32s31` when needed.
|
||||
|
||||
`pytest-embedded` stores per-test logs under `$IDF_PATH/pytest-embedded/`. The script writes the reconstructed image to `jpeg_decode_result.ppm` inside that test log directory, so after the test finishes you can open the generated `PPM` file locally with an image viewer that supports `PPM` to inspect the decoded output.
|
||||
|
||||
## Replacing The Embedded JPEG Asset
|
||||
|
||||
If you want to try another input image, replace:
|
||||
|
||||
- `main/assets/image.jpg`
|
||||
|
||||
Keep the replacement as a baseline JPEG with the same general scale if you want UART log volume and test runtime to stay small.
|
||||
|
||||
After replacing the asset, rerun the example and update `golden_output.ppm` from the generated `dut.logdir/jpeg_decode_result.ppm` artifact if the new output should become the expected result.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
(For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you as soon as possible.)
|
||||
(For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you as soon as possible.)
|
||||
|
||||
Binary file not shown.
@@ -1,3 +1,5 @@
|
||||
idf_component_register(SRCS "jpeg_decode_main.c"
|
||||
PRIV_REQUIRES fatfs esp_driver_jpeg esp_psram
|
||||
INCLUDE_DIRS ".")
|
||||
idf_component_register(SRCS "jpeg_decode_example_main.c"
|
||||
PRIV_REQUIRES esp_driver_jpeg mbedtls
|
||||
INCLUDE_DIRS ".")
|
||||
|
||||
target_add_binary_data(${COMPONENT_LIB} "${CMAKE_CURRENT_LIST_DIR}/assets/image.jpg" BINARY RENAME_TO "example_jpeg")
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
menu "JPEG Decode Example menu"
|
||||
|
||||
config EXAMPLE_FORMAT_IF_MOUNT_FAILED
|
||||
bool "Format the card if mount failed"
|
||||
default n
|
||||
help
|
||||
If this config item is set, format_if_mount_failed will be set to true and the card will be formatted if
|
||||
the mount has failed.
|
||||
|
||||
config EXAMPLE_SDMMC_IO_POWER_INTERNAL_LDO
|
||||
depends on SOC_SDMMC_IO_POWER_EXTERNAL
|
||||
bool "SDMMC IO power supply comes from internal LDO (READ HELP!)"
|
||||
default y
|
||||
help
|
||||
Please read the schematic first and check if the SDMMC VDD is connected to any internal LDO output.
|
||||
If the SDMMC is powered by an external supplier, unselect me
|
||||
|
||||
endmenu
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "sdkconfig.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "driver/jpeg_decode.h"
|
||||
#include "mbedtls/base64.h"
|
||||
|
||||
#define EXAMPLE_BASE64_CHUNK_LEN 96
|
||||
#define EXAMPLE_BYTES_PER_PIXEL 3
|
||||
|
||||
/* These linker symbols are generated automatically for the file added by
|
||||
* EMBED_FILES in CMakeLists.txt. They let the example treat the embedded
|
||||
* JPEG asset as a byte array stored in flash. */
|
||||
extern const uint8_t example_jpeg_start[] asm("_binary_example_jpeg_start");
|
||||
extern const uint8_t example_jpeg_end[] asm("_binary_example_jpeg_end");
|
||||
|
||||
/* For JPEGs encoded with block-based chroma subsampling, the decoder output
|
||||
* buffer dimensions can be padded up to 16-pixel boundaries. This helper
|
||||
* rounds visible width or height up to that padded size. */
|
||||
static uint32_t align_up_to_16(uint32_t value)
|
||||
{
|
||||
return (value + 15U) & ~15U;
|
||||
}
|
||||
|
||||
static void print_base64_payload(const unsigned char *encoded, size_t encoded_len)
|
||||
{
|
||||
/* The payload is split into short lines so the UART log stays easy to
|
||||
* parse from pytest and less likely to be damaged by very long lines. */
|
||||
printf("JPEG_DECODE_BASE64_BEGIN\n");
|
||||
size_t chunk_idx = 0;
|
||||
for (size_t offset = 0; offset < encoded_len; offset += EXAMPLE_BASE64_CHUNK_LEN, ++chunk_idx) {
|
||||
size_t chunk_len = encoded_len - offset;
|
||||
if (chunk_len > EXAMPLE_BASE64_CHUNK_LEN) {
|
||||
chunk_len = EXAMPLE_BASE64_CHUNK_LEN;
|
||||
}
|
||||
printf("JPEG_DECODE_BASE64 %.*s\n", (int)chunk_len, (const char *)&encoded[offset]);
|
||||
/* Yield periodically to avoid watchdog triggers on long payloads. */
|
||||
if ((chunk_idx % 16U) == 15U) {
|
||||
vTaskDelay(1);
|
||||
}
|
||||
}
|
||||
printf("JPEG_DECODE_BASE64_END\n");
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
const size_t embedded_size = example_jpeg_end - example_jpeg_start;
|
||||
jpeg_decoder_handle_t jpeg_handle = NULL;
|
||||
uint8_t *decoded_pixels = NULL;
|
||||
uint8_t *input_buf = NULL;
|
||||
unsigned char *encoded = NULL;
|
||||
|
||||
printf("Loading embedded JPEG from flash...\n");
|
||||
printf("Embedded JPEG size: %zu bytes\n", embedded_size);
|
||||
|
||||
/* Parse the JPEG header to learn the image dimensions. */
|
||||
jpeg_decode_picture_info_t header_info;
|
||||
ESP_ERROR_CHECK(jpeg_decoder_get_info(example_jpeg_start, embedded_size, &header_info));
|
||||
printf("JPEG header parsed: width=%" PRIu32 " height=%" PRIu32 "\n", header_info.width, header_info.height);
|
||||
|
||||
/* The hardware decoder can pad the output image dimensions up to
|
||||
* 16-pixel boundaries, so the actual buffer may be larger than
|
||||
* width * height * 3. Allocate for the padded dimensions to avoid
|
||||
* out-of-bounds writes. */
|
||||
const uint32_t padded_width = align_up_to_16(header_info.width);
|
||||
const uint32_t padded_height = align_up_to_16(header_info.height);
|
||||
const uint32_t output_bytes = padded_width * padded_height * EXAMPLE_BYTES_PER_PIXEL;
|
||||
|
||||
/* Ask the driver to allocate an output buffer for decoded pixels.
|
||||
* JPEG_DEC_ALLOC_OUTPUT_BUFFER tells the driver this memory will hold
|
||||
* the decode result (as opposed to an input bitstream buffer). */
|
||||
jpeg_decode_memory_alloc_cfg_t mem_cfg = {
|
||||
.buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER,
|
||||
};
|
||||
size_t decoded_buffer_size = 0;
|
||||
decoded_pixels = (uint8_t *)jpeg_alloc_decoder_mem(output_bytes, &mem_cfg, &decoded_buffer_size);
|
||||
assert(decoded_pixels != NULL);
|
||||
|
||||
/* Create a decoder engine instance. The timeout_ms value is the maximum
|
||||
* time the hardware is allowed to spend on a single decode call. */
|
||||
jpeg_decode_engine_cfg_t decode_eng_cfg = {
|
||||
.timeout_ms = 80,
|
||||
};
|
||||
ESP_ERROR_CHECK(jpeg_new_decoder_engine(&decode_eng_cfg, &jpeg_handle));
|
||||
|
||||
/* RGB888 outputs 3 bytes per pixel. BGR order matches the default byte
|
||||
* layout expected by OpenCV and many display pipelines. */
|
||||
jpeg_decode_cfg_t decode_cfg = {
|
||||
.output_format = JPEG_DECODE_OUT_FORMAT_RGB888,
|
||||
.rgb_order = JPEG_DEC_RGB_ELEMENT_ORDER_BGR,
|
||||
};
|
||||
|
||||
/* jpeg don't handle the encrypted data.*/
|
||||
const uint8_t *bit_stream = example_jpeg_start;
|
||||
#if CONFIG_SECURE_FLASH_ENC_ENABLED
|
||||
size_t input_buffer_size = 0;
|
||||
jpeg_decode_memory_alloc_cfg_t in_mem_cfg = {
|
||||
.buffer_direction = JPEG_DEC_ALLOC_INPUT_BUFFER,
|
||||
};
|
||||
input_buf = (uint8_t *)jpeg_alloc_decoder_mem(embedded_size, &in_mem_cfg, &input_buffer_size);
|
||||
assert(input_buf != NULL);
|
||||
memcpy(input_buf, example_jpeg_start, embedded_size);
|
||||
bit_stream = input_buf;
|
||||
#endif
|
||||
|
||||
uint32_t decoded_size = 0;
|
||||
printf("Decoding JPEG -> RGB888...\n");
|
||||
ESP_ERROR_CHECK(jpeg_decoder_process(
|
||||
jpeg_handle,
|
||||
&decode_cfg,
|
||||
bit_stream,
|
||||
embedded_size,
|
||||
decoded_pixels,
|
||||
decoded_buffer_size,
|
||||
&decoded_size
|
||||
));
|
||||
printf("Decoded RGB888 size: %" PRIu32 " bytes\n", decoded_size);
|
||||
|
||||
/* Base64 turns the binary pixel data into printable ASCII so it can be
|
||||
* safely transported through the serial console and reconstructed by
|
||||
* pytest. The two-pass pattern (first call with NULL output to get the
|
||||
* required buffer size, then the real encode) is standard mbedtls usage. */
|
||||
size_t encoded_len = 0;
|
||||
int ret = mbedtls_base64_encode(NULL, 0, &encoded_len, decoded_pixels, decoded_size);
|
||||
ESP_ERROR_CHECK((ret == MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) ? ESP_OK : ESP_FAIL);
|
||||
encoded = calloc(encoded_len + 1, 1);
|
||||
assert(encoded != NULL);
|
||||
ESP_ERROR_CHECK(mbedtls_base64_encode(encoded, encoded_len + 1, &encoded_len, decoded_pixels, decoded_size) == 0 ? ESP_OK : ESP_FAIL);
|
||||
|
||||
/* JPEG_DECODE_INFO plus the chunked JPEG_DECODE_BASE64 lines form a tiny
|
||||
* text protocol that the pytest script understands and converts back
|
||||
* into a PPM golden file for comparison. */
|
||||
printf("JPEG_DECODE_INFO width=%" PRIu32 " height=%" PRIu32
|
||||
" padded_width=%" PRIu32 " padded_height=%" PRIu32
|
||||
" format=RGB888 encoding=base64 size=%" PRIu32 "\n",
|
||||
header_info.width, header_info.height, padded_width, padded_height, decoded_size);
|
||||
print_base64_payload(encoded, encoded_len);
|
||||
printf("JPEG decode demo done.\n");
|
||||
|
||||
ESP_ERROR_CHECK(jpeg_del_decoder_engine(jpeg_handle));
|
||||
free(encoded);
|
||||
free(decoded_pixels);
|
||||
free(input_buf);
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_vfs_fat.h"
|
||||
#include "sdmmc_cmd.h"
|
||||
#include "driver/sdmmc_host.h"
|
||||
#include "esp_attr.h"
|
||||
#include "driver/jpeg_decode.h"
|
||||
#include "sd_pwr_ctrl_by_on_chip_ldo.h"
|
||||
|
||||
static const char *TAG = "jpeg.example";
|
||||
static sdmmc_card_t *s_card;
|
||||
#define MOUNT_POINT "/sdcard"
|
||||
|
||||
const static char jpg_file_1080[] = "/sdcard/esp1080.jpg";
|
||||
const static char raw_file_1080[] = "/sdcard/out.rgb";
|
||||
const static char jpg_file_720[] = "/sdcard/esp720.jpg";
|
||||
const static char raw_file_720[] = "/sdcard/out2.rgb";
|
||||
|
||||
static esp_err_t sdcard_init(void)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
|
||||
#ifdef CONFIG_EXAMPLE_FORMAT_IF_MOUNT_FAILED
|
||||
.format_if_mount_failed = true,
|
||||
#else
|
||||
.format_if_mount_failed = false,
|
||||
#endif // EXAMPLE_FORMAT_IF_MOUNT_FAILED
|
||||
.max_files = 5,
|
||||
.allocation_unit_size = 16 * 1024
|
||||
};
|
||||
const char mount_point[] = MOUNT_POINT;
|
||||
ESP_LOGI(TAG, "Initializing SD card");
|
||||
|
||||
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
|
||||
host.max_freq_khz = SDMMC_FREQ_HIGHSPEED;
|
||||
|
||||
#if CONFIG_EXAMPLE_SDMMC_IO_POWER_INTERNAL_LDO
|
||||
sd_pwr_ctrl_ldo_config_t ldo_config = {
|
||||
.ldo_chan_id = 4, // `LDO_VO4` is used as the SDMMC IO power
|
||||
};
|
||||
sd_pwr_ctrl_handle_t pwr_ctrl_handle = NULL;
|
||||
|
||||
ret = sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &pwr_ctrl_handle);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to new an on-chip ldo power control driver");
|
||||
return ret;
|
||||
}
|
||||
host.pwr_ctrl_handle = pwr_ctrl_handle;
|
||||
#endif
|
||||
|
||||
// This initializes the slot without card detect (CD) and write protect (WP) signals.
|
||||
// Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals.
|
||||
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
|
||||
slot_config.width = 4;
|
||||
slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
|
||||
|
||||
ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &s_card);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
if (ret == ESP_FAIL) {
|
||||
ESP_LOGE(TAG, "Failed to mount filesystem. "
|
||||
"If you want the card to be formatted, set the EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option.");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to initialize the card (%s). "
|
||||
"Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
// Card has been initialized, print its properties
|
||||
sdmmc_card_print_info(stdout, s_card);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void sdcard_deinit(void)
|
||||
{
|
||||
const char mount_point[] = MOUNT_POINT;
|
||||
esp_vfs_fat_sdcard_unmount(mount_point, s_card);
|
||||
#if SOC_SDMMC_IO_POWER_EXTERNAL
|
||||
esp_err_t ret = sd_pwr_ctrl_del_on_chip_ldo(s_card->host.pwr_ctrl_handle);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to delete on-chip ldo power control driver");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_ERROR_CHECK(sdcard_init());
|
||||
|
||||
jpeg_decoder_handle_t jpgd_handle;
|
||||
|
||||
jpeg_decode_engine_cfg_t decode_eng_cfg = {
|
||||
.timeout_ms = 40,
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(jpeg_new_decoder_engine(&decode_eng_cfg, &jpgd_handle));
|
||||
|
||||
jpeg_decode_cfg_t decode_cfg_rgb = {
|
||||
.output_format = JPEG_DECODE_OUT_FORMAT_RGB888,
|
||||
.rgb_order = JPEG_DEC_RGB_ELEMENT_ORDER_BGR,
|
||||
};
|
||||
|
||||
jpeg_decode_cfg_t decode_cfg_gray = {
|
||||
.output_format = JPEG_DECODE_OUT_FORMAT_GRAY,
|
||||
};
|
||||
|
||||
jpeg_decode_memory_alloc_cfg_t rx_mem_cfg = {
|
||||
.buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER,
|
||||
};
|
||||
|
||||
jpeg_decode_memory_alloc_cfg_t tx_mem_cfg = {
|
||||
.buffer_direction = JPEG_DEC_ALLOC_INPUT_BUFFER,
|
||||
};
|
||||
|
||||
FILE *file_jpg_1080p = fopen(jpg_file_1080, "rb");
|
||||
ESP_LOGI(TAG, "jpg_file_1080:%s", jpg_file_1080);
|
||||
if (file_jpg_1080p == NULL) {
|
||||
ESP_LOGE(TAG, "fopen file_jpg_1080p error");
|
||||
return;
|
||||
}
|
||||
|
||||
fseek(file_jpg_1080p, 0, SEEK_END);
|
||||
int jpeg_size_1080p = ftell(file_jpg_1080p);
|
||||
fseek(file_jpg_1080p, 0, SEEK_SET);
|
||||
size_t tx_buffer_size_1080p = 0;
|
||||
uint8_t *tx_buf_1080p = (uint8_t*)jpeg_alloc_decoder_mem(jpeg_size_1080p, &tx_mem_cfg, &tx_buffer_size_1080p);
|
||||
if (tx_buf_1080p == NULL) {
|
||||
ESP_LOGE(TAG, "alloc 1080p tx buffer error");
|
||||
return;
|
||||
}
|
||||
fread(tx_buf_1080p, 1, jpeg_size_1080p, file_jpg_1080p);
|
||||
fclose(file_jpg_1080p);
|
||||
|
||||
FILE *file_jpg_720p = fopen(jpg_file_720, "rb");
|
||||
ESP_LOGI(TAG, "jpg_file_1080:%s", jpg_file_720);
|
||||
if (file_jpg_720p == NULL) {
|
||||
ESP_LOGE(TAG, "fopen file_jpg_720p error");
|
||||
return;
|
||||
}
|
||||
fseek(file_jpg_720p, 0, SEEK_END);
|
||||
int jpeg_size_720p = ftell(file_jpg_720p);
|
||||
fseek(file_jpg_720p, 0, SEEK_SET);
|
||||
size_t tx_buffer_size_720p = 0;
|
||||
uint8_t *tx_buf_720p = (uint8_t*)jpeg_alloc_decoder_mem(jpeg_size_720p, &tx_mem_cfg, &tx_buffer_size_720p);
|
||||
if (tx_buf_720p == NULL) {
|
||||
ESP_LOGE(TAG, "alloc 720p tx buffer error");
|
||||
return;
|
||||
}
|
||||
fread(tx_buf_720p, 1, jpeg_size_720p, file_jpg_720p);
|
||||
fclose(file_jpg_720p);
|
||||
|
||||
size_t rx_buffer_size_1080p = 0;
|
||||
size_t rx_buffer_size_720p = 0;
|
||||
uint8_t *rx_buf_1080p = (uint8_t*)jpeg_alloc_decoder_mem(1920 * 1088 * 3, &rx_mem_cfg, &rx_buffer_size_1080p);
|
||||
uint8_t *rx_buf_720p = (uint8_t*)jpeg_alloc_decoder_mem(720 * 1280, &rx_mem_cfg, &rx_buffer_size_720p);
|
||||
if (rx_buf_1080p == NULL) {
|
||||
ESP_LOGE(TAG, "alloc 1080p rx buffer error");
|
||||
return;
|
||||
}
|
||||
if (rx_buf_720p == NULL) {
|
||||
ESP_LOGE(TAG, "alloc 720p rx buffer error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the jpg header information (This step is optional)
|
||||
jpeg_decode_picture_info_t header_info;
|
||||
ESP_ERROR_CHECK(jpeg_decoder_get_info(tx_buf_1080p, jpeg_size_1080p, &header_info));
|
||||
ESP_LOGI(TAG, "header parsed, width is %" PRId32 ", height is %" PRId32, header_info.width, header_info.height);
|
||||
|
||||
uint32_t out_size_1080p = 0;
|
||||
uint32_t out_size_720p = 0;
|
||||
ESP_ERROR_CHECK(jpeg_decoder_process(jpgd_handle, &decode_cfg_rgb, tx_buf_1080p, jpeg_size_1080p, rx_buf_1080p, rx_buffer_size_1080p, &out_size_1080p));
|
||||
ESP_ERROR_CHECK(jpeg_decoder_process(jpgd_handle, &decode_cfg_gray, tx_buf_720p, jpeg_size_720p, rx_buf_720p, rx_buffer_size_720p, &out_size_720p));
|
||||
|
||||
// Write two pictures.
|
||||
FILE *file_rgb_1080p = fopen(raw_file_1080, "wb");
|
||||
ESP_LOGI(TAG, "raw_file_1080:%s", raw_file_1080);
|
||||
if (file_rgb_1080p == NULL) {
|
||||
ESP_LOGE(TAG, "fopen file_rgb_1080p error");
|
||||
return;
|
||||
}
|
||||
fwrite(rx_buf_1080p, 1, out_size_1080p, file_rgb_1080p);
|
||||
fclose(file_rgb_1080p);
|
||||
|
||||
FILE *file_rgb_720p = fopen(raw_file_720, "wb");
|
||||
ESP_LOGI(TAG, "raw_file_720:%s", raw_file_720);
|
||||
if (file_rgb_720p == NULL) {
|
||||
ESP_LOGE(TAG, "fopen file_rgb_720p error");
|
||||
return;
|
||||
}
|
||||
fwrite(rx_buf_720p, 1, out_size_720p, file_rgb_720p);
|
||||
fclose(file_rgb_720p);
|
||||
|
||||
sdcard_deinit();
|
||||
ESP_LOGI(TAG, "Card unmounted");
|
||||
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
import argparse
|
||||
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
|
||||
def open_picture(path): # type: (str) -> list[int]
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
f.close()
|
||||
new_data = [int(x) for x in data]
|
||||
return new_data
|
||||
|
||||
|
||||
def picture_show_rgb888(data, h, w): # type: (list[int], int, int) -> None
|
||||
data = np.array(data).reshape(h, w, 3).astype(np.uint8)
|
||||
cv.imshow('data', data)
|
||||
cv.waitKey()
|
||||
|
||||
|
||||
def picture_show_rgb565(data, h, w): # type: (list[int], int, int) -> None
|
||||
|
||||
new_data = [0] * ((len(data) // 2) * 3)
|
||||
for i in range(len(data)):
|
||||
if i % 2 != 0:
|
||||
new_data[3 * (i - 1) // 2 + 2] = (data[i] & 0xf8)
|
||||
new_data[3 * (i - 1) // 2 + 1] |= (data[i] & 0x7) << 5
|
||||
else:
|
||||
new_data[3 * i // 2] = (data[i] & 0x1f) << 3
|
||||
new_data[3 * i // 2 + 1] |= (data[i] & 0xe0) >> 3
|
||||
|
||||
new_data = np.array(new_data).reshape(h, w, 3).astype(np.uint8)
|
||||
cv.imshow('data', new_data)
|
||||
cv.waitKey()
|
||||
|
||||
|
||||
def picture_show_gray(data, h, w): # type: (list[int], int, int) -> None
|
||||
new_data = np.array(data).reshape(h, w, 1).astype(np.uint8)
|
||||
cv.imshow('data', new_data)
|
||||
cv.waitKey()
|
||||
|
||||
|
||||
def convert_YUV_to_RGB(Y, U, V): # type: (NDArray, NDArray, NDArray) -> tuple[NDArray, NDArray, NDArray]
|
||||
B = np.clip(Y + 1.7790 * (U - 128), 0, 255).astype(np.uint8)
|
||||
G = np.clip(Y - 0.3455 * (U - 128) - 0.7169 * (V - 128), 0, 255).astype(np.uint8)
|
||||
R = np.clip(Y + 1.4075 * (V - 128), 0, 255).astype(np.uint8)
|
||||
|
||||
return B, G, R
|
||||
|
||||
|
||||
def picture_show_yuv420(data, h, w): # type: (list[int], int, int) -> None
|
||||
new_u = [0] * (h * w)
|
||||
new_v = [0] * (h * w)
|
||||
new_y = [0] * (h * w)
|
||||
|
||||
for i in range(int(h * w * 1.5)):
|
||||
is_even_row = ((i // (w * 1.5)) % 2 == 0)
|
||||
if is_even_row:
|
||||
if (i % 3 == 0):
|
||||
new_u[(i // 3) * 2] = data[i]
|
||||
new_u[(i // 3) * 2 + 1] = data[i]
|
||||
else:
|
||||
if (i % 3 == 0):
|
||||
new_u[(i // 3) * 2] = new_u[int((i - (w * 1.5)) // 3) * 2]
|
||||
new_u[(i // 3) * 2 + 1] = new_u[int((i - (w * 1.5)) // 3) * 2 + 1]
|
||||
|
||||
for i in range(int(h * w * 1.5)):
|
||||
if (i // (w * 1.5)) % 2 != 0 and (i % 3 == 0):
|
||||
idx = (i // 3) * 2
|
||||
new_v[idx] = data[i]
|
||||
new_v[idx + 1] = data[i]
|
||||
|
||||
for i in range(int(h * w * 1.5)):
|
||||
if (i // (w * 1.5)) % 2 == 0 and (i % 3 == 0):
|
||||
idx = (i // 3) * 2
|
||||
new_v[idx] = new_v[int((i + (w * 1.5)) // 3) * 2]
|
||||
new_v[idx + 1] = new_v[int((i + (w * 1.5)) // 3) * 2 + 1]
|
||||
|
||||
new_y = [data[i] for i in range(int(h * w * 1.5)) if i % 3 != 0]
|
||||
|
||||
Y = np.array(new_y)
|
||||
U = np.array(new_u)
|
||||
V = np.array(new_v)
|
||||
|
||||
B, G, R = convert_YUV_to_RGB(Y, U, V)
|
||||
# Merge channels
|
||||
new_data = np.stack((B, G, R), axis=-1)
|
||||
new_data = np.array(new_data).reshape(h, w, 3).astype(np.uint8)
|
||||
|
||||
# Display the image
|
||||
cv.imshow('data', new_data)
|
||||
cv.waitKey()
|
||||
|
||||
|
||||
def picture_show_yuv422(data, h, w): # type: (list[int], int, int) -> None
|
||||
# Reshape the input data to a 2D array
|
||||
data_array = np.array(data).reshape(h, w * 2)
|
||||
|
||||
# Separate Y, U, and V channels
|
||||
Y = data_array[:, 1::2]
|
||||
U = data_array[:, 0::4].repeat(2, axis=1)
|
||||
V = data_array[:, 2::4].repeat(2, axis=1)
|
||||
|
||||
# Convert YUV to RGB
|
||||
B, G, R = convert_YUV_to_RGB(Y, U, V)
|
||||
|
||||
# Merge channels
|
||||
new_data = np.stack((B, G, R), axis=-1)
|
||||
|
||||
# Display the image
|
||||
cv.imshow('data', new_data)
|
||||
cv.waitKey()
|
||||
|
||||
|
||||
def picture_show_yuv444(data, h, w): # type: (list[int], int, int) -> None
|
||||
# Reshape the input data to a 2D array
|
||||
data_array = np.array(data).reshape(h, w * 3)
|
||||
|
||||
# Separate Y, U, and V channels
|
||||
Y = data_array[:, 2::3]
|
||||
U = data_array[:, 1::3]
|
||||
V = data_array[:, 0::3]
|
||||
|
||||
# Convert YUV to RGB
|
||||
B, G, R = convert_YUV_to_RGB(Y, U, V)
|
||||
|
||||
# Merge channels
|
||||
new_data = np.stack((B, G, R), axis=-1)
|
||||
|
||||
# Display the image
|
||||
cv.imshow('data', new_data)
|
||||
cv.waitKey()
|
||||
|
||||
|
||||
def main(): # type: () -> None
|
||||
parser = argparse.ArgumentParser(description='which mode need to show')
|
||||
|
||||
parser.add_argument(
|
||||
'--pic_path',
|
||||
type=str,
|
||||
help='What is the path of your picture',
|
||||
required=True)
|
||||
|
||||
parser.add_argument(
|
||||
'--pic_type',
|
||||
type=str,
|
||||
help='What type you want to show',
|
||||
required=True,
|
||||
choices=['rgb565', 'rgb888', 'gray', 'yuv422', 'yuv420', 'yuv444'])
|
||||
|
||||
parser.add_argument(
|
||||
'--height',
|
||||
type=int,
|
||||
help='the picture height',
|
||||
default=480)
|
||||
|
||||
parser.add_argument(
|
||||
'--width',
|
||||
type=int,
|
||||
help='the picture width',
|
||||
default=640)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
height = args.height
|
||||
width = args.width
|
||||
|
||||
data = open_picture(args.pic_path)
|
||||
if (args.pic_type == 'rgb565'):
|
||||
picture_show_rgb565(data, height, width)
|
||||
elif (args.pic_type == 'rgb888'):
|
||||
picture_show_rgb888(data, height, width)
|
||||
elif (args.pic_type == 'gray'):
|
||||
picture_show_gray(data, height, width)
|
||||
elif (args.pic_type == 'yuv420'):
|
||||
picture_show_yuv420(data, height, width)
|
||||
elif (args.pic_type == 'yuv422'):
|
||||
picture_show_yuv422(data, height, width)
|
||||
elif (args.pic_type == 'yuv444'):
|
||||
picture_show_yuv444(data, height, width)
|
||||
else:
|
||||
print('This type is not supported in this script!')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,240 @@
|
||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from pytest_embedded import Dut
|
||||
from pytest_embedded_idf.utils import idf_parametrize
|
||||
from pytest_embedded_idf.utils import soc_filtered_targets
|
||||
|
||||
DECODE_OUTPUT_NAME = 'jpeg_decode_result.ppm'
|
||||
GOLDEN_OUTPUT_NAME = 'golden_output.ppm'
|
||||
GOLDEN_OUTPUT_PATH = Path(__file__).with_name(GOLDEN_OUTPUT_NAME)
|
||||
EXPECTED_PIXEL_FORMAT = 'RGB888'
|
||||
EXPECTED_ENCODING = 'base64'
|
||||
RGB888_BYTES_PER_PIXEL = 3
|
||||
PPM_MAGIC = b'P6'
|
||||
PPM_MAX_VALUE = b'255'
|
||||
DECODE_INFO_PATTERN = (
|
||||
r'JPEG_DECODE_INFO width=(?P<width>\d+) height=(?P<height>\d+) '
|
||||
r'padded_width=(?P<padded_width>\d+) padded_height=(?P<padded_height>\d+) '
|
||||
r'format=(?P<format>\w+) encoding=(?P<encoding>\w+) size=(?P<size>\d+)'
|
||||
)
|
||||
DECODE_INFO_RE = re.compile(DECODE_INFO_PATTERN)
|
||||
DECODE_CHUNK_PATTERN = r'JPEG_DECODE_BASE64 (?P<payload>[A-Za-z0-9+/=]+)'
|
||||
DECODE_CHUNK_RE = re.compile(DECODE_CHUNK_PATTERN)
|
||||
PPM_HEADER_RE = re.compile(rb'^P6\s+(?P<width>\d+)\s+(?P<height>\d+)\s+(?P<max_value>\d+)\s')
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DecodeMetadata:
|
||||
width: int
|
||||
height: int
|
||||
padded_width: int
|
||||
padded_height: int
|
||||
pixel_format: str
|
||||
encoding: str
|
||||
size: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.width <= 0 or self.height <= 0:
|
||||
raise ValueError(f'Invalid dimensions: {self.width}x{self.height}')
|
||||
if self.padded_width < self.width or self.padded_height < self.height:
|
||||
raise ValueError(
|
||||
f'Padded size ({self.padded_width}x{self.padded_height}) '
|
||||
f'smaller than visible size ({self.width}x{self.height})'
|
||||
)
|
||||
if self.pixel_format != EXPECTED_PIXEL_FORMAT:
|
||||
raise ValueError(f'Unsupported pixel format: {self.pixel_format}')
|
||||
if self.encoding != EXPECTED_ENCODING:
|
||||
raise ValueError(f'Unsupported encoding: {self.encoding}')
|
||||
|
||||
@property
|
||||
def padded_image_size(self) -> int:
|
||||
return self.padded_width * self.padded_height * RGB888_BYTES_PER_PIXEL
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RgbImage:
|
||||
width: int
|
||||
height: int
|
||||
pixels_rgb888: bytes
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
expected_size = self.width * self.height * RGB888_BYTES_PER_PIXEL
|
||||
if len(self.pixels_rgb888) != expected_size:
|
||||
raise ValueError(f'Expected {expected_size} RGB bytes, got {len(self.pixels_rgb888)}')
|
||||
|
||||
|
||||
def parse_decode_metadata(meta_line: str) -> DecodeMetadata:
|
||||
match = DECODE_INFO_RE.fullmatch(meta_line)
|
||||
if not match:
|
||||
raise ValueError(f'Invalid decode metadata line: {meta_line}')
|
||||
|
||||
return DecodeMetadata(
|
||||
width=int(match.group('width')),
|
||||
height=int(match.group('height')),
|
||||
padded_width=int(match.group('padded_width')),
|
||||
padded_height=int(match.group('padded_height')),
|
||||
pixel_format=match.group('format'),
|
||||
encoding=match.group('encoding'),
|
||||
size=int(match.group('size')),
|
||||
)
|
||||
|
||||
|
||||
def collect_base64_payload(dut: Dut) -> List[str]:
|
||||
payload_lines: List[str] = []
|
||||
while True:
|
||||
# The example prints the decoded frame as multiple short UART lines
|
||||
# instead of one giant base64 blob, so collect and join them here.
|
||||
match = dut.expect(rf'(?P<line>JPEG_DECODE_BASE64_END|{DECODE_CHUNK_PATTERN}\r?\n)', timeout=60)
|
||||
line = match.group('line').decode('utf-8').strip()
|
||||
if line == 'JPEG_DECODE_BASE64_END':
|
||||
return payload_lines
|
||||
|
||||
chunk_match = DECODE_CHUNK_RE.fullmatch(line)
|
||||
assert chunk_match is not None
|
||||
payload_lines.append(chunk_match.group('payload'))
|
||||
|
||||
|
||||
def _crop_visible_bgr888(raw_bytes: bytes, metadata: DecodeMetadata) -> bytes:
|
||||
# The hardware can write into a padded decode buffer whose width/height are
|
||||
# rounded up to JPEG block boundaries. Pytest only wants the visible image,
|
||||
# so keep the useful bytes from each row and discard the padded tail rows.
|
||||
if len(raw_bytes) != metadata.padded_image_size:
|
||||
raise ValueError(f'Expected {metadata.padded_image_size} padded BGR bytes, got {len(raw_bytes)}')
|
||||
|
||||
visible_row_size = metadata.width * RGB888_BYTES_PER_PIXEL
|
||||
padded_row_size = metadata.padded_width * RGB888_BYTES_PER_PIXEL
|
||||
return b''.join(
|
||||
raw_bytes[offset : offset + visible_row_size]
|
||||
for offset in range(0, padded_row_size * metadata.height, padded_row_size)
|
||||
)
|
||||
|
||||
|
||||
def _bgr888_to_rgb888(raw_bytes: bytes) -> bytes:
|
||||
# The decoder's RGB888 mode uses BGR24 byte layout by default. Swap the
|
||||
# first and third byte in each pixel so the PPM artifact becomes standard
|
||||
# RGB order that common desktop image tools expect.
|
||||
rgb_bytes = bytearray(raw_bytes)
|
||||
rgb_bytes[0::3], rgb_bytes[2::3] = raw_bytes[2::3], raw_bytes[0::3]
|
||||
return bytes(rgb_bytes)
|
||||
|
||||
|
||||
def decode_base64_image(metadata: DecodeMetadata, payload_lines: List[str]) -> RgbImage:
|
||||
# The DUT sends the raw decode buffer as base64 over UART because the test
|
||||
# environment only observes text logs. Rebuild bytes on the host, crop away
|
||||
# decoder padding, then normalize the pixel order for image comparison.
|
||||
raw_bytes = base64.b64decode(''.join(payload_lines), validate=True)
|
||||
if len(raw_bytes) != metadata.size:
|
||||
raise ValueError(f'Expected {metadata.size} decoded bytes, got {len(raw_bytes)}')
|
||||
|
||||
visible_bgr888 = _crop_visible_bgr888(raw_bytes, metadata)
|
||||
return RgbImage(
|
||||
width=metadata.width,
|
||||
height=metadata.height,
|
||||
pixels_rgb888=_bgr888_to_rgb888(visible_bgr888),
|
||||
)
|
||||
|
||||
|
||||
def _encode_ppm(image: RgbImage) -> bytes:
|
||||
header = b'%s\n%d %d\n%s\n' % (PPM_MAGIC, image.width, image.height, PPM_MAX_VALUE)
|
||||
return header + image.pixels_rgb888
|
||||
|
||||
|
||||
def _load_ppm(path: Path) -> RgbImage:
|
||||
ppm_bytes = path.read_bytes()
|
||||
header_match = PPM_HEADER_RE.match(ppm_bytes)
|
||||
if not header_match:
|
||||
raise ValueError('Invalid PPM header')
|
||||
|
||||
width = int(header_match.group('width'))
|
||||
height = int(header_match.group('height'))
|
||||
max_value = header_match.group('max_value')
|
||||
if width <= 0 or height <= 0:
|
||||
raise ValueError('Unsupported PPM dimensions')
|
||||
if max_value != PPM_MAX_VALUE:
|
||||
raise ValueError(f'Unsupported PPM max value: {max_value.decode("ascii", errors="replace")}')
|
||||
|
||||
pixel_data = ppm_bytes[header_match.end() :]
|
||||
return RgbImage(width=width, height=height, pixels_rgb888=pixel_data)
|
||||
|
||||
|
||||
def save_ppm_artifact(image: RgbImage, output_path: Path) -> None:
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
output_path.write_bytes(_encode_ppm(image))
|
||||
except OSError:
|
||||
logging.exception('Failed to save JPEG decode artifact to %s', output_path)
|
||||
return
|
||||
|
||||
logging.info('Saved JPEG decode artifact to %s', output_path)
|
||||
|
||||
|
||||
def image_digest(image: RgbImage) -> str:
|
||||
digest = hashlib.sha256()
|
||||
digest.update(image.width.to_bytes(4, 'big'))
|
||||
digest.update(image.height.to_bytes(4, 'big'))
|
||||
digest.update(image.pixels_rgb888)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def assert_image_matches_golden(result_image: RgbImage, golden_path: Path) -> None:
|
||||
assert golden_path.is_file(), f'Golden PPM not found: {golden_path}'
|
||||
golden_image = _load_ppm(golden_path)
|
||||
|
||||
assert image_digest(result_image) == image_digest(golden_image), (
|
||||
f'Generated image does not match golden file: {golden_path.name}'
|
||||
)
|
||||
|
||||
|
||||
def run_jpeg_decode_example(dut: Dut) -> None:
|
||||
dut.expect_exact('Loading embedded JPEG from flash...')
|
||||
dut.expect(r'Embedded JPEG size: \d+ bytes')
|
||||
dut.expect(r'JPEG header parsed: width=\d+ height=\d+')
|
||||
dut.expect_exact('Decoding JPEG -> RGB888...')
|
||||
dut.expect(r'Decoded RGB888 size: \d+ bytes')
|
||||
|
||||
metadata_line = dut.expect(DECODE_INFO_PATTERN).group(0).decode('utf-8')
|
||||
metadata = parse_decode_metadata(metadata_line)
|
||||
|
||||
dut.expect_exact('JPEG_DECODE_BASE64_BEGIN')
|
||||
# Collect the machine-readable payload before the example prints its final
|
||||
# completion line so we keep the UART parsing strictly in output order.
|
||||
payload_lines = collect_base64_payload(dut)
|
||||
dut.expect_exact('JPEG decode demo done.')
|
||||
|
||||
result_image = decode_base64_image(metadata, payload_lines)
|
||||
output_path = Path(dut.logdir) / DECODE_OUTPUT_NAME
|
||||
save_ppm_artifact(result_image, output_path)
|
||||
assert_image_matches_golden(result_image, GOLDEN_OUTPUT_PATH)
|
||||
|
||||
|
||||
@pytest.mark.generic
|
||||
@idf_parametrize('target', soc_filtered_targets('SOC_JPEG_DECODE_SUPPORTED == 1'), indirect=['target'])
|
||||
def test_jpeg_decode_example(dut: Dut) -> None:
|
||||
run_jpeg_decode_example(dut)
|
||||
|
||||
|
||||
@pytest.mark.flash_encryption
|
||||
@pytest.mark.parametrize(
|
||||
'config',
|
||||
[
|
||||
'flash_enc',
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
@idf_parametrize(
|
||||
'target',
|
||||
soc_filtered_targets('SOC_JPEG_DECODE_SUPPORTED == 1 and SOC_FLASH_ENC_SUPPORTED == 1'),
|
||||
indirect=['target'],
|
||||
)
|
||||
def test_jpeg_decode_example_with_flash_encryption(dut: Dut) -> None:
|
||||
run_jpeg_decode_example(dut)
|
||||
@@ -1,2 +0,0 @@
|
||||
opencv-python
|
||||
numpy
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB |
@@ -0,0 +1 @@
|
||||
# Default CI build, inherits sdkconfig.defaults
|
||||
@@ -0,0 +1,7 @@
|
||||
CONFIG_PARTITION_TABLE_OFFSET=0x9000
|
||||
CONFIG_SECURE_FLASH_ENC_ENABLED=y
|
||||
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT=y
|
||||
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_ENC=y
|
||||
CONFIG_SECURE_FLASH_REQUIRE_ALREADY_ENABLED=y
|
||||
CONFIG_SPIRAM_ENC_EXEMPT=y
|
||||
CONFIG_SPIRAM_ENC_EXEMPT_SIZE=4096
|
||||
@@ -1,6 +1 @@
|
||||
# SPIRAM configurations
|
||||
|
||||
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
|
||||
CONFIG_SPIRAM=y
|
||||
CONFIG_SPIRAM_MODE_HEX=y
|
||||
CONFIG_SPIRAM_SPEED_200M=y
|
||||
|
||||
@@ -30,6 +30,44 @@ See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/l
|
||||
|
||||
## Example Output
|
||||
|
||||
```text
|
||||
Loading embedded BGR24 image from flash...
|
||||
Embedded raw image size: 2764800 bytes
|
||||
Encoding BGR24(raw) -> JPEG...
|
||||
Encoded JPEG size: 30795 bytes
|
||||
JPEG_META width=1280 height=720 format=JPEG encoding=base64 size=30795
|
||||
JPEG_BASE64_BEGIN
|
||||
JPEG_BASE64 ...
|
||||
JPEG_BASE64 ...
|
||||
JPEG_BASE64_END
|
||||
JPEG encode demo done.
|
||||
```
|
||||
|
||||
## Pytest Visual Check
|
||||
|
||||
The accompanying `pytest_jpeg_encode.py` script captures the `JPEG_META` and `JPEG_BASE64` output, reconstructs the encoded JPEG, and saves it as:
|
||||
|
||||
- `dut.logdir/jpeg_encode_result.jpeg`
|
||||
|
||||
It also compares the generated JPEG with `golden_output.jpeg`. This turns the example into both a functional regression test and a host-side artifact generator that makes the encoded result easy to inspect.
|
||||
|
||||
## Running Pytest Locally And Viewing The Image
|
||||
|
||||
To run the pytest helper locally on hardware, build the example for your target first, then invoke the test script with the target and serial port:
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32p4 build
|
||||
pytest --target esp32p4 --port PORT pytest_jpeg_encode.py
|
||||
```
|
||||
|
||||
Replace `esp32p4` with another supported target such as `esp32s31` when needed.
|
||||
|
||||
`pytest-embedded` stores per-test logs under `$IDF_PATH/pytest-embedded/`. The script writes the reconstructed image to `jpeg_encode_result.jpeg` inside that test log directory, so after the test finishes you can open the generated JPEG locally with any image viewer to inspect the encoded output.
|
||||
|
||||
## Replacing The Embedded RGB Asset
|
||||
|
||||
If you want to regenerate a compatible raw frame from another input image, one simple workflow is:
|
||||
|
||||
```bash
|
||||
I (1114) jpeg.example: Initializing SD card
|
||||
I (1114) gpio: GPIO[43]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -1,3 +1,5 @@
|
||||
idf_component_register(SRCS "jpeg_encode_main.c"
|
||||
PRIV_REQUIRES fatfs esp_driver_jpeg
|
||||
idf_component_register(SRCS "jpeg_encode_example_main.c"
|
||||
PRIV_REQUIRES fatfs esp_driver_jpeg mbedtls esp_psram
|
||||
INCLUDE_DIRS ".")
|
||||
|
||||
target_add_binary_data(${COMPONENT_LIB} "${CMAKE_CURRENT_LIST_DIR}/assets/esp720p.rgb" BINARY RENAME_TO "esp720p_rgb")
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
menu "JPEG Encode Example menu"
|
||||
|
||||
config EXAMPLE_FORMAT_IF_MOUNT_FAILED
|
||||
bool "Format the card if mount failed"
|
||||
default n
|
||||
help
|
||||
If this config item is set, format_if_mount_failed will be set to true and the card will be formatted if
|
||||
the mount has failed.
|
||||
|
||||
config EXAMPLE_SDMMC_IO_POWER_INTERNAL_LDO
|
||||
depends on SOC_SDMMC_IO_POWER_EXTERNAL
|
||||
bool "SDMMC IO power supply comes from internal LDO (READ HELP!)"
|
||||
default y
|
||||
help
|
||||
Please read the schematic first and check if the SDMMC VDD is connected to any internal LDO output.
|
||||
If the SDMMC is powered by an external supplier, unselect me
|
||||
|
||||
endmenu
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#include <assert.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "mbedtls/base64.h"
|
||||
#include "esp_check.h"
|
||||
#include "driver/jpeg_encode.h"
|
||||
|
||||
#define EXAMPLE_WIDTH 1280
|
||||
#define EXAMPLE_HEIGHT 720
|
||||
#define EXAMPLE_RGB_FRAME_SIZE (EXAMPLE_WIDTH * EXAMPLE_HEIGHT * 3)
|
||||
#define EXAMPLE_JPEG_BUFFER_SIZE (EXAMPLE_RGB_FRAME_SIZE / 10) /* Estimate output size with an approximately 10:1 JPEG compression ratio for this demo image. */
|
||||
#define EXAMPLE_BASE64_CHUNK_LEN 96
|
||||
#define EXAMPLE_JPEG_QUALITY 80
|
||||
|
||||
extern const uint8_t esp720p_rgb_start[] asm("_binary_esp720p_rgb_start");
|
||||
extern const uint8_t esp720p_rgb_end[] asm("_binary_esp720p_rgb_end");
|
||||
|
||||
static void print_base64_payload(const unsigned char *encoded, size_t encoded_len)
|
||||
{
|
||||
/* Split the printable payload into short lines so it is easy to read in
|
||||
* the serial monitor and robust for pytest to parse back into a JPEG. */
|
||||
printf("JPEG_BASE64_BEGIN\n");
|
||||
for (size_t offset = 0; offset < encoded_len; offset += EXAMPLE_BASE64_CHUNK_LEN) {
|
||||
size_t chunk_len = encoded_len - offset;
|
||||
if (chunk_len > EXAMPLE_BASE64_CHUNK_LEN) {
|
||||
chunk_len = EXAMPLE_BASE64_CHUNK_LEN;
|
||||
}
|
||||
printf("JPEG_BASE64 %.*s\n", (int)chunk_len, (const char *)&encoded[offset]);
|
||||
}
|
||||
printf("JPEG_BASE64_END\n");
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
/* EMBED_FILES turns the raw asset into linker symbols, so the example can
|
||||
* read the picture directly from flash without mounting a filesystem. */
|
||||
const size_t embedded_size = esp720p_rgb_end - esp720p_rgb_start;
|
||||
uint32_t jpeg_size = 0;
|
||||
jpeg_encoder_handle_t jpeg_handle = NULL;
|
||||
uint8_t *rgb_buf = NULL;
|
||||
|
||||
printf("Loading embedded BGR24 image from flash...\n");
|
||||
printf("Embedded raw image size: %zu bytes\n", embedded_size);
|
||||
assert(embedded_size == EXAMPLE_RGB_FRAME_SIZE);
|
||||
|
||||
/* Despite the enum name, the current driver maps
|
||||
* JPEG_ENCODE_IN_FORMAT_RGB888 to a BGR24-style byte layout.
|
||||
* Keep the embedded raw asset in bgr24 order or red/blue will swap. */
|
||||
jpeg_encode_cfg_t enc_config = {
|
||||
.src_type = JPEG_ENCODE_IN_FORMAT_RGB888,
|
||||
.sub_sample = JPEG_DOWN_SAMPLING_YUV422,
|
||||
.image_quality = EXAMPLE_JPEG_QUALITY,
|
||||
.width = EXAMPLE_WIDTH,
|
||||
.height = EXAMPLE_HEIGHT,
|
||||
};
|
||||
|
||||
const uint8_t *rgb_src = esp720p_rgb_start;
|
||||
#if CONFIG_SECURE_FLASH_ENC_ENABLED
|
||||
size_t input_buffer_size = 0;
|
||||
jpeg_encode_memory_alloc_cfg_t rx_mem_cfg = {
|
||||
.buffer_direction = JPEG_ENC_ALLOC_INPUT_BUFFER,
|
||||
};
|
||||
rgb_buf = (uint8_t *)jpeg_alloc_encoder_mem(EXAMPLE_RGB_FRAME_SIZE, &rx_mem_cfg, &input_buffer_size);
|
||||
assert(rgb_buf != NULL);
|
||||
memcpy(rgb_buf, esp720p_rgb_start, EXAMPLE_RGB_FRAME_SIZE);
|
||||
rgb_src = rgb_buf;
|
||||
#endif
|
||||
|
||||
size_t result_buffer_size = 0;
|
||||
/* The output JPEG is compressed, so the example does not need to reserve
|
||||
* a full raw-frame worth of space for the bitstream. This 10:1 estimate
|
||||
* is intentionally conservative for the bundled demo image and quality
|
||||
* setting; real applications should size this buffer for their own worst
|
||||
* case and handle "buffer too small" errors if needed. */
|
||||
jpeg_encode_memory_alloc_cfg_t mem_cfg = {
|
||||
.buffer_direction = JPEG_ENC_ALLOC_OUTPUT_BUFFER,
|
||||
};
|
||||
uint8_t *jpeg_buf = (uint8_t *)jpeg_alloc_encoder_mem(EXAMPLE_JPEG_BUFFER_SIZE, &mem_cfg, &result_buffer_size);
|
||||
assert(jpeg_buf != NULL);
|
||||
|
||||
/* Create the encoder instance once, then feed it one full-frame request. */
|
||||
jpeg_encode_engine_cfg_t encode_eng_cfg = {
|
||||
.timeout_ms = 200,
|
||||
};
|
||||
ESP_ERROR_CHECK(jpeg_new_encoder_engine(&encode_eng_cfg, &jpeg_handle));
|
||||
|
||||
printf("Encoding BGR24(raw) -> JPEG...\n");
|
||||
ESP_ERROR_CHECK(jpeg_encoder_process(jpeg_handle, &enc_config, rgb_src, EXAMPLE_RGB_FRAME_SIZE,
|
||||
jpeg_buf, result_buffer_size, &jpeg_size));
|
||||
printf("Encoded JPEG size: %" PRIu32 " bytes\n", jpeg_size);
|
||||
|
||||
size_t encoded_len = 0;
|
||||
/* First call asks mbedTLS how big the base64 buffer must be, then the
|
||||
* second call performs the actual binary-to-text conversion. */
|
||||
int ret = mbedtls_base64_encode(NULL, 0, &encoded_len, jpeg_buf, jpeg_size);
|
||||
ESP_ERROR_CHECK((ret == MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) ? ESP_OK : ESP_FAIL);
|
||||
unsigned char *encoded = calloc(encoded_len + 1, 1);
|
||||
assert(encoded != NULL);
|
||||
ESP_ERROR_CHECK(mbedtls_base64_encode(encoded, encoded_len + 1, &encoded_len, jpeg_buf, jpeg_size) == 0 ? ESP_OK : ESP_FAIL);
|
||||
|
||||
/* JPEG_META plus the chunked JPEG_BASE64 lines form a tiny text protocol
|
||||
* that pytest understands and can reconstruct into a host-side .jpeg. */
|
||||
printf("JPEG_META width=%u height=%u format=JPEG encoding=base64 size=%" PRIu32 "\n",
|
||||
EXAMPLE_WIDTH, EXAMPLE_HEIGHT, jpeg_size);
|
||||
print_base64_payload(encoded, encoded_len);
|
||||
printf("JPEG encode demo done.\n");
|
||||
|
||||
ESP_ERROR_CHECK(jpeg_del_encoder_engine(jpeg_handle));
|
||||
free(encoded);
|
||||
free(jpeg_buf);
|
||||
free(rgb_buf);
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_vfs_fat.h"
|
||||
#include "sdmmc_cmd.h"
|
||||
#include "driver/sdmmc_host.h"
|
||||
#include "driver/jpeg_encode.h"
|
||||
#include "sd_pwr_ctrl_by_on_chip_ldo.h"
|
||||
|
||||
static const char *TAG = "jpeg.example";
|
||||
static sdmmc_card_t *s_card;
|
||||
#define MOUNT_POINT "/sdcard"
|
||||
|
||||
const static char s_infile_1080p[] = "/sdcard/esp1080.rgb";
|
||||
const static char s_outfile_1080p[] = "/sdcard/outjpg.jpg";
|
||||
|
||||
static esp_err_t sdcard_init(void)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
|
||||
#ifdef CONFIG_EXAMPLE_FORMAT_IF_MOUNT_FAILED
|
||||
.format_if_mount_failed = true,
|
||||
#else
|
||||
.format_if_mount_failed = false,
|
||||
#endif // EXAMPLE_FORMAT_IF_MOUNT_FAILED
|
||||
.max_files = 5,
|
||||
.allocation_unit_size = 16 * 1024
|
||||
};
|
||||
const char mount_point[] = MOUNT_POINT;
|
||||
ESP_LOGI(TAG, "Initializing SD card");
|
||||
|
||||
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
|
||||
host.max_freq_khz = SDMMC_FREQ_HIGHSPEED;
|
||||
|
||||
#if CONFIG_EXAMPLE_SDMMC_IO_POWER_INTERNAL_LDO
|
||||
sd_pwr_ctrl_ldo_config_t ldo_config = {
|
||||
.ldo_chan_id = 4, // `LDO_VO4` is used as the SDMMC IO power
|
||||
};
|
||||
sd_pwr_ctrl_handle_t pwr_ctrl_handle = NULL;
|
||||
|
||||
ret = sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &pwr_ctrl_handle);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to new an on-chip ldo power control driver");
|
||||
return ret;
|
||||
}
|
||||
host.pwr_ctrl_handle = pwr_ctrl_handle;
|
||||
#endif
|
||||
|
||||
// This initializes the slot without card detect (CD) and write protect (WP) signals.
|
||||
// Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals.
|
||||
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
|
||||
slot_config.width = 4;
|
||||
slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
|
||||
|
||||
ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &s_card);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
if (ret == ESP_FAIL) {
|
||||
ESP_LOGE(TAG, "Failed to mount filesystem. "
|
||||
"If you want the card to be formatted, set the EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option.");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to initialize the card (%s). "
|
||||
"Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
// Card has been initialized, print its properties
|
||||
sdmmc_card_print_info(stdout, s_card);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void sdcard_deinit(void)
|
||||
{
|
||||
const char mount_point[] = MOUNT_POINT;
|
||||
esp_vfs_fat_sdcard_unmount(mount_point, s_card);
|
||||
#if SOC_SDMMC_IO_POWER_EXTERNAL
|
||||
esp_err_t ret = sd_pwr_ctrl_del_on_chip_ldo(s_card->host.pwr_ctrl_handle);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to delete on-chip ldo power control driver");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_ERROR_CHECK(sdcard_init());
|
||||
uint32_t raw_size_1080p;
|
||||
uint32_t jpg_size_1080p;
|
||||
jpeg_encoder_handle_t jpeg_handle;
|
||||
|
||||
FILE *file_raw_1080p = fopen(s_infile_1080p, "rb");
|
||||
ESP_LOGI(TAG, "s_infile_1080p:%s", s_infile_1080p);
|
||||
if (file_raw_1080p == NULL) {
|
||||
ESP_LOGE(TAG, "fopen file_raw_1080p error");
|
||||
return;
|
||||
}
|
||||
|
||||
jpeg_encode_engine_cfg_t encode_eng_cfg = {
|
||||
.timeout_ms = 70,
|
||||
};
|
||||
|
||||
jpeg_encode_memory_alloc_cfg_t rx_mem_cfg = {
|
||||
.buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER,
|
||||
};
|
||||
|
||||
jpeg_encode_memory_alloc_cfg_t tx_mem_cfg = {
|
||||
.buffer_direction = JPEG_DEC_ALLOC_INPUT_BUFFER,
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(jpeg_new_encoder_engine(&encode_eng_cfg, &jpeg_handle));
|
||||
// Read 1080p raw picture
|
||||
fseek(file_raw_1080p, 0, SEEK_END);
|
||||
raw_size_1080p = ftell(file_raw_1080p);
|
||||
fseek(file_raw_1080p, 0, SEEK_SET);
|
||||
size_t tx_buffer_size = 0;
|
||||
uint8_t *raw_buf_1080p = (uint8_t*)jpeg_alloc_encoder_mem(raw_size_1080p, &tx_mem_cfg, &tx_buffer_size);
|
||||
assert(raw_buf_1080p != NULL);
|
||||
fread(raw_buf_1080p, 1, raw_size_1080p, file_raw_1080p);
|
||||
fclose(file_raw_1080p);
|
||||
|
||||
size_t rx_buffer_size = 0;
|
||||
uint8_t *jpg_buf_1080p = (uint8_t*)jpeg_alloc_encoder_mem(raw_size_1080p / 10, &rx_mem_cfg, &rx_buffer_size); // Assume that compression ratio of 10 to 1
|
||||
assert(jpg_buf_1080p != NULL);
|
||||
|
||||
jpeg_encode_cfg_t enc_config = {
|
||||
.src_type = JPEG_ENCODE_IN_FORMAT_RGB888,
|
||||
.sub_sample = JPEG_DOWN_SAMPLING_YUV422,
|
||||
.image_quality = 80,
|
||||
.width = 1920,
|
||||
.height = 1080,
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(jpeg_encoder_process(jpeg_handle, &enc_config, raw_buf_1080p, raw_size_1080p, jpg_buf_1080p, rx_buffer_size, &jpg_size_1080p));
|
||||
|
||||
FILE *file_jpg_1080p = fopen(s_outfile_1080p, "wb");
|
||||
ESP_LOGI(TAG, "outfile:%s", s_outfile_1080p);
|
||||
if (file_jpg_1080p == NULL) {
|
||||
ESP_LOGE(TAG, "fopen file_jpg_1080p error");
|
||||
return;
|
||||
}
|
||||
|
||||
fwrite(jpg_buf_1080p, 1, jpg_size_1080p, file_jpg_1080p);
|
||||
fclose(file_jpg_1080p);
|
||||
|
||||
sdcard_deinit();
|
||||
ESP_LOGI(TAG, "Card unmounted");
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, , 0x6000,
|
||||
phy_init, data, phy, , 0x1000,
|
||||
factory, app, factory, , 0x380000,
|
||||
|
@@ -0,0 +1,146 @@
|
||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from pytest_embedded import Dut
|
||||
from pytest_embedded_idf.utils import idf_parametrize
|
||||
from pytest_embedded_idf.utils import soc_filtered_targets
|
||||
|
||||
JPEG_META_PATTERN = r'JPEG_META width=(\d+) height=(\d+) format=(\w+) encoding=(\w+) size=(\d+)'
|
||||
JPEG_META_RE = re.compile(rf'^{JPEG_META_PATTERN}$')
|
||||
JPEG_CHUNK_RE = re.compile(r'^JPEG_BASE64 ([A-Za-z0-9+/=]+)$')
|
||||
JPEG_OUTPUT_NAME = 'jpeg_encode_result.jpeg'
|
||||
GOLDEN_IMAGE_NAME = 'golden_output.jpeg'
|
||||
GOLDEN_IMAGE_PATH = Path(__file__).with_name(GOLDEN_IMAGE_NAME)
|
||||
EXPECTED_FORMAT = 'JPEG'
|
||||
EXPECTED_ENCODING = 'base64'
|
||||
EXPECTED_WIDTH = 1280
|
||||
EXPECTED_HEIGHT = 720
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JpegMetadata:
|
||||
width: int
|
||||
height: int
|
||||
image_format: str
|
||||
encoding: str
|
||||
size: int
|
||||
|
||||
|
||||
def parse_jpeg_metadata(meta_line: str) -> JpegMetadata:
|
||||
match = JPEG_META_RE.match(meta_line)
|
||||
if not match:
|
||||
raise ValueError(f'Invalid JPEG metadata line: {meta_line}')
|
||||
|
||||
return JpegMetadata(
|
||||
width=int(match.group(1)),
|
||||
height=int(match.group(2)),
|
||||
image_format=match.group(3),
|
||||
encoding=match.group(4),
|
||||
size=int(match.group(5)),
|
||||
)
|
||||
|
||||
|
||||
def collect_base64_payload(dut: Dut) -> List[str]:
|
||||
payload_chunks: List[str] = []
|
||||
while True:
|
||||
match = dut.expect(r'(JPEG_BASE64_END|JPEG_BASE64 [A-Za-z0-9+/=]+\r?\n)')
|
||||
line = match.group(1).decode('utf-8').strip()
|
||||
if line == 'JPEG_BASE64_END':
|
||||
return payload_chunks
|
||||
|
||||
chunk_match = JPEG_CHUNK_RE.match(line)
|
||||
assert chunk_match is not None
|
||||
payload_chunks.append(chunk_match.group(1))
|
||||
|
||||
|
||||
def decode_jpeg_base64_payload(metadata: JpegMetadata, payload_lines: List[str]) -> bytes:
|
||||
if metadata.image_format != EXPECTED_FORMAT:
|
||||
raise ValueError(f'Unsupported image format: {metadata.image_format}')
|
||||
if metadata.encoding != EXPECTED_ENCODING:
|
||||
raise ValueError(f'Unsupported payload encoding: {metadata.encoding}')
|
||||
|
||||
jpeg_bytes = base64.b64decode(''.join(payload_lines), validate=True)
|
||||
if len(jpeg_bytes) != metadata.size:
|
||||
raise ValueError(f'Expected {metadata.size} JPEG bytes, got {len(jpeg_bytes)}')
|
||||
|
||||
return jpeg_bytes
|
||||
|
||||
|
||||
def save_jpeg_artifact(jpeg_bytes: bytes, output_path: Path) -> None:
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
output_path.write_bytes(jpeg_bytes)
|
||||
except OSError:
|
||||
logging.exception('Failed to save JPEG artifact to %s', output_path)
|
||||
return
|
||||
|
||||
logging.info('Saved JPEG artifact to %s', output_path)
|
||||
|
||||
|
||||
def _sha256_digest(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def assert_jpeg_matches_golden(result_bytes: bytes, golden_path: Path) -> None:
|
||||
assert golden_path.is_file(), f'Golden JPEG not found: {golden_path}'
|
||||
golden_bytes = golden_path.read_bytes()
|
||||
result_digest = _sha256_digest(result_bytes)
|
||||
golden_digest = _sha256_digest(golden_bytes)
|
||||
|
||||
assert result_digest == golden_digest, (
|
||||
f'Generated JPEG does not match golden file: {golden_path.name} '
|
||||
f'(result sha256={result_digest}, golden sha256={golden_digest})'
|
||||
)
|
||||
|
||||
|
||||
def run_jpeg_encode_example(dut: Dut) -> None:
|
||||
dut.expect_exact('Loading embedded BGR24 image from flash...')
|
||||
dut.expect(r'Embedded raw image size: \d+ bytes')
|
||||
dut.expect_exact('Encoding BGR24(raw) -> JPEG...')
|
||||
dut.expect(r'Encoded JPEG size: \d+ bytes')
|
||||
|
||||
metadata = parse_jpeg_metadata(dut.expect(JPEG_META_PATTERN).group(0).decode('utf-8'))
|
||||
assert metadata.width == EXPECTED_WIDTH
|
||||
assert metadata.height == EXPECTED_HEIGHT
|
||||
|
||||
dut.expect_exact('JPEG_BASE64_BEGIN')
|
||||
payload_lines = collect_base64_payload(dut)
|
||||
|
||||
jpeg_bytes = decode_jpeg_base64_payload(metadata, payload_lines)
|
||||
output_path = Path(dut.logdir) / JPEG_OUTPUT_NAME
|
||||
save_jpeg_artifact(jpeg_bytes, output_path)
|
||||
assert_jpeg_matches_golden(jpeg_bytes, GOLDEN_IMAGE_PATH)
|
||||
|
||||
dut.expect_exact('JPEG encode demo done.')
|
||||
|
||||
|
||||
@pytest.mark.generic
|
||||
@idf_parametrize('target', soc_filtered_targets('SOC_JPEG_ENCODE_SUPPORTED == 1'), indirect=['target'])
|
||||
def test_jpeg_encode_example(dut: Dut) -> None:
|
||||
run_jpeg_encode_example(dut)
|
||||
|
||||
|
||||
@pytest.mark.flash_encryption
|
||||
@pytest.mark.parametrize(
|
||||
'config',
|
||||
[
|
||||
'flash_enc',
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
@idf_parametrize(
|
||||
'target',
|
||||
soc_filtered_targets('SOC_JPEG_ENCODE_SUPPORTED == 1 and SOC_FLASH_ENC_SUPPORTED == 1'),
|
||||
indirect=['target'],
|
||||
)
|
||||
def test_jpeg_encode_example_with_flash_encryption(dut: Dut) -> None:
|
||||
run_jpeg_encode_example(dut)
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
# Default CI build, inherits sdkconfig.defaults
|
||||
@@ -0,0 +1,7 @@
|
||||
CONFIG_PARTITION_TABLE_OFFSET=0x9000
|
||||
CONFIG_SECURE_FLASH_ENC_ENABLED=y
|
||||
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT=y
|
||||
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_ENC=y
|
||||
CONFIG_SECURE_FLASH_REQUIRE_ALREADY_ENABLED=y
|
||||
CONFIG_SPIRAM_ENC_EXEMPT=y
|
||||
CONFIG_SPIRAM_ENC_EXEMPT_SIZE=4096
|
||||
@@ -0,0 +1,8 @@
|
||||
# SPIRAM configurations
|
||||
|
||||
CONFIG_SPIRAM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
|
||||
@@ -1,6 +0,0 @@
|
||||
# SPIRAM configurations
|
||||
|
||||
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
|
||||
CONFIG_SPIRAM=y
|
||||
CONFIG_SPIRAM_MODE_HEX=y
|
||||
CONFIG_SPIRAM_SPEED_200M=y
|
||||
Reference in New Issue
Block a user