mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
feat(jpeg): simplify decoder example and add pytest coverage
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.)
|
||||
|
||||
BIN
examples/peripherals/jpeg/jpeg_decode/golden_output.ppm
Normal file
BIN
examples/peripherals/jpeg/jpeg_decode/golden_output.ppm
Normal file
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
|
||||
BIN
examples/peripherals/jpeg/jpeg_decode/main/assets/image.jpg
Normal file
BIN
examples/peripherals/jpeg/jpeg_decode/main/assets/image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -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()
|
||||
@@ -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 |
@@ -1,6 +1 @@
|
||||
# SPIRAM configurations
|
||||
|
||||
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
|
||||
CONFIG_SPIRAM=y
|
||||
CONFIG_SPIRAM_MODE_HEX=y
|
||||
CONFIG_SPIRAM_SPEED_200M=y
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user