feat(jpeg): simplify decoder example and add pytest coverage

This commit is contained in:
morris
2026-06-16 10:38:58 +08:00
committed by C.S.M
parent 98bdf38ad6
commit c2aa496f98
17 changed files with 93 additions and 275 deletions

View File

@@ -241,7 +241,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:
- *common_components
- esp_driver_jpeg

View File

@@ -5,4 +5,4 @@ cmake_minimum_required(VERSION 3.22)
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)

View File

@@ -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.)

View File

@@ -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")

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -1,6 +1 @@
# SPIRAM configurations
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_HEX=y
CONFIG_SPIRAM_SPEED_200M=y

View File

@@ -50,6 +50,19 @@ The accompanying `pytest_jpeg_encode.py` script captures the `JPEG_META` and `JP
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: