feat(ppa): add blend with color key example

This commit is contained in:
morris
2026-06-22 21:48:05 +08:00
parent 1e2b539c1c
commit 8548b5cfa8
13 changed files with 3706 additions and 0 deletions

View File

@@ -32,6 +32,7 @@ repos:
.*.yuv|
.*.rgb|
.*.gray|
.*.ppm|
.*COPYING.*|
docs/sphinx-known-warnings\.txt
)$

View File

@@ -176,6 +176,7 @@ Application Examples
^^^^^^^^^^^^^^^^^^^^
* :example:`peripherals/ppa/ppa_transform` - PPA transform image processing example. The embedded RGB565 image is transformed by SRM, highlighted with blend, framed with fill, and emitted as base64 for host-side PPM reconstruction and golden-image comparison.
* :example:`peripherals/ppa/ppa_color_key` - PPA blend color-keying example. The example generates a centered RGB888 glow foreground in software, then demonstrates two blend effects on the embedded RGB565 image: replacing the keyed red `ESP32` text with the glow, and preserving the keyed text while blending the glow into the non-key area. Both results are emitted as base64 for host-side PPM reconstruction and golden-image comparison.
API Reference
-------------

View File

@@ -176,6 +176,7 @@ PPA 操作作用于输入图片的目标块。因此,完成一次 PPA 事务
^^^^^^^^^
* :example:`peripherals/ppa/ppa_transform` - PPA transform 图像处理示例。嵌入的 RGB565 图像会经过 SRM 变换、blend 高亮和 fill 边框处理,然后以 base64 输出,供主机端重建为 PPM 并与 golden 图像比对。
* :example:`peripherals/ppa/ppa_color_key` - PPA blend color key 示例。示例先在软件中生成居中的 RGB888 glow 前景,然后在嵌入式 RGB565 图片上演示两种 blend 效果:一种是通过 blend color key 将命中的红色 `ESP32` 文本像素替换为 glow另一种是在保留命中文本的同时将 glow 混合到非 key 区域。两种结果都会以 base64 输出,供主机端重建为 PPM 并与 golden 图像比对。
API 参考
--------

View File

@@ -519,6 +519,16 @@ examples/peripherals/pcnt:
- esp_driver_pcnt
- soc
examples/peripherals/ppa/ppa_color_key:
disable:
- if: SOC_PPA_SUPPORTED != 1
depends_components:
- esp_driver_dma
- esp_driver_ppa
- esp_hal_ppa
- mbedtls
- soc
examples/peripherals/ppa/ppa_transform:
disable:
- if: SOC_PPA_SUPPORTED != 1

View File

@@ -0,0 +1,8 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
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(ppa_color_key)

View File

@@ -0,0 +1,81 @@
| Supported Targets | ESP32-P4 | ESP32-S31 |
| ----------------- | -------- | --------- |
# PPA Color Key Example
## Overview
This example demonstrates two common color-keying effects of the PPA blend engine without any display hardware.
The example is intentionally tailored to its embedded demo asset. It embeds a pre-generated raw RGB565 image, generates a centered RGB888 glow-style foreground image in software, then runs two PPA blend color-keying passes. The first pass replaces the red `ESP32` text with the generated glow. The second pass keeps the red keyed text untouched while the remaining area blends with the same generated effect. The glow geometry is fixed because the demo asset keeps the text near the center of the frame. Both final RGB565 buffers are base64-encoded and printed to the serial console. The accompanying pytest script reconstructs the images as PPM files and compares them with two golden reference images.
The processing pipeline demonstrates:
- Software preprocessing: generate a fixed centered RGB888 glow foreground
- Blend color key effect 1: replace the red keyed text with the generated RGB888 glow
- Blend color key effect 2: preserve the red keyed text while blending the generated RGB888 glow into the non-key area
## Hardware Required
* An ESP development board with PPA support
* An USB cable for power supply and programming
## Build and Flash
Run `idf.py -p PORT build flash monitor` to build, flash and monitor the project.
(To exit the serial monitor, type ``Ctrl-]``.)
See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects.
## Example Output
```text
I (1555) main_task: Calling app_main()
Loading embedded RGB565 image from flash...
Embedded raw image size: 153600 bytes
Generating shared glow foreground...
Replacing keyed red text with glow foreground...
IMAGE_META effect=replace_keyed_text_with_glow width=320 height=240 format=RGB565 encoding=base64
IMAGE_BASE64_BEGIN
IMAGE_BASE64 ...
IMAGE_BASE64 ...
IMAGE_BASE64_END
Blending glow around keyed red text...
IMAGE_META effect=blend_glow_around_keyed_text width=320 height=240 format=RGB565 encoding=base64
IMAGE_BASE64_BEGIN
IMAGE_BASE64 ...
IMAGE_BASE64 ...
IMAGE_BASE64_END
PPA color key demo done.
I (10085) main_task: Returned from app_main()
```
## Pytest Visual Check
The accompanying `pytest_ppa_color_key.py` script captures each `IMAGE_META` and `IMAGE_BASE64` payload, reconstructs both processed images, and saves them as:
- `dut.logdir/ppa_color_key_replace_keyed_text_with_glow.ppm`
- `dut.logdir/ppa_color_key_blend_glow_around_keyed_text.ppm`
It also compares the generated images with `golden_replace_keyed_text_with_glow.ppm` and `golden_blend_glow_around_keyed_text.ppm` by hashing the decoded RGB pixel content. This turns the example into a functional regression test and a visual artifact generator for CI logs.
### Getting The PPM Result Locally
If you want to inspect the processed image on your computer, first build the example for your target, then run pytest from the ESP-IDF root directory with the matching target and serial port:
```bash
pytest -k test_ppa_color_key --target esp32p4 --port PORT
```
Replace `esp32p4` with another supported target such as `esp32s31`, and set `PORT` to your board's serial device.
`pytest-embedded` stores per-test logs under `$IDF_PATH/pytest-embedded/`. When the test finishes, pytest prints a log line similar to:
```text
Saved PPA artifact to .../pytest-embedded/<timestamp>/esp32p4.default.test_ppa_color_key/ppa_color_key_replace_keyed_text_with_glow.ppm
Saved PPA artifact to .../pytest-embedded/<timestamp>/esp32p4.default.test_ppa_color_key/ppa_color_key_blend_glow_around_keyed_text.ppm
```
You can open both generated PPM files from that log directory with any image viewer to inspect the two PPA color-key effects locally.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
idf_component_register(SRCS "ppa_color_key_example_main.c"
INCLUDE_DIRS "."
REQUIRES esp_mm esp_psram esp_driver_ppa mbedtls)
target_add_binary_data(${COMPONENT_LIB} "${CMAKE_CURRENT_LIST_DIR}/assets/image.rgb" BINARY RENAME_TO "image_rgb")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,296 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include "driver/ppa.h"
#include "esp_check.h"
#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mbedtls/base64.h"
#define EXAMPLE_IMAGE_WIDTH 320
#define EXAMPLE_IMAGE_HEIGHT 240
#define EXAMPLE_RGB565_PIXEL_SIZE 2
#define EXAMPLE_RGB888_PIXEL_SIZE 3
#define EXAMPLE_RGB565_IMAGE_SIZE (EXAMPLE_IMAGE_WIDTH * EXAMPLE_IMAGE_HEIGHT * EXAMPLE_RGB565_PIXEL_SIZE)
#define EXAMPLE_RGB888_IMAGE_SIZE (EXAMPLE_IMAGE_WIDTH * EXAMPLE_IMAGE_HEIGHT * EXAMPLE_RGB888_PIXEL_SIZE)
#define EXAMPLE_BASE64_CHUNK_LEN 96
#define EXAMPLE_RED_KEY_MIN_R 0xA0
#define EXAMPLE_RED_KEY_MAX_G 0x78
#define EXAMPLE_RED_KEY_MAX_B 0x78
#define EXAMPLE_GLOW_BLEND_ALPHA 176
#define EXAMPLE_GLOW_CENTER_X 160
#define EXAMPLE_GLOW_CENTER_Y 120
#define EXAMPLE_GLOW_RADIUS_X 160
#define EXAMPLE_GLOW_RADIUS_Y 60
#define EXAMPLE_GLOW_HIGHLIGHT_X 120
#define EXAMPLE_GLOW_PHASE_Y 90
/* The raw RGB565 asset is embedded by target_add_binary_data() in CMakeLists.txt.
* RENAME_TO "image_rgb" gives the linker symbols below. */
extern const uint8_t image_rgb_start[] asm("_binary_image_rgb_start");
extern const uint8_t image_rgb_end[] asm("_binary_image_rgb_end");
static uint8_t clamp_u8(int value)
{
if (value < 0) {
return 0;
}
if (value > 0xFF) {
return 0xFF;
}
return (uint8_t)value;
}
static uint8_t *alloc_ppa_buffer(size_t size)
{
/* PPA uses DMA underneath, so input and output buffers must be DMA-capable
* and cache-line aligned. PSRAM keeps the example friendly to boards with
* limited internal RAM. */
uint8_t *buffer = heap_caps_aligned_calloc(64, 1, size, MALLOC_CAP_DMA | MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
assert(buffer);
return buffer;
}
static void print_base64_payload(const unsigned char *encoded, size_t encoded_len)
{
printf("IMAGE_BASE64_BEGIN\n");
size_t chunk_count = 0;
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("IMAGE_BASE64 %.*s\n", (int)chunk_len, (const char *)&encoded[offset]);
chunk_count++;
if ((chunk_count % 16) == 0) {
/* The complete payload is large. Yield periodically so the serial
* monitor used by pytest can consume every line reliably. */
vTaskDelay(pdMS_TO_TICKS(10));
}
}
printf("IMAGE_BASE64_END\n");
}
static void prepare_glow_foreground(uint8_t *fg_buf)
{
/* The foreground is generated in software so the example can blend a
* modern glow effect into the whole scene without needing a second image
* asset. The text is centered in this demo image, so the glow shape can
* use fixed geometry instead of dynamically scanning the picture. */
uint8_t *pixels = fg_buf;
for (int y = 0; y < EXAMPLE_IMAGE_HEIGHT; y++) {
for (int x = 0; x < EXAMPLE_IMAGE_WIDTH; x++) {
const int dx = x - EXAMPLE_GLOW_CENTER_X;
const int dy = y - EXAMPLE_GLOW_CENTER_Y;
const int ellipse_x = (dx * dx * 255) / (EXAMPLE_GLOW_RADIUS_X * EXAMPLE_GLOW_RADIUS_X);
const int ellipse_y = (dy * dy * 255) / (EXAMPLE_GLOW_RADIUS_Y * EXAMPLE_GLOW_RADIUS_Y);
int halo = 255 - ellipse_x - ellipse_y;
if (halo < 0) {
halo = 0;
}
if (halo > 255) {
halo = 255;
}
const int shimmer_dx = abs(x - EXAMPLE_GLOW_HIGHLIGHT_X);
int shimmer = 255 - (shimmer_dx * 255) / EXAMPLE_GLOW_RADIUS_X;
if (shimmer < 0) {
shimmer = 0;
}
int intensity = halo + shimmer / 3;
if (intensity > 255) {
intensity = 255;
}
int red = 96 + (255 - intensity) * 36 / 255 + shimmer / 18;
int green = 138 + intensity * 110 / 255;
int blue = 208 + intensity * 47 / 255;
if ((((y - EXAMPLE_GLOW_PHASE_Y) / 5) & 1) == 0) {
green += 4;
blue += 8;
}
/* PPA_BLEND_COLOR_MODE_RGB888 maps to BGR24 in memory. */
size_t pixel_index = (y * EXAMPLE_IMAGE_WIDTH + x) * EXAMPLE_RGB888_PIXEL_SIZE;
pixels[pixel_index] = clamp_u8(blue);
pixels[pixel_index + 1] = clamp_u8(green);
pixels[pixel_index + 2] = clamp_u8(red);
}
}
}
static void replace_keyed_text_with_glow(ppa_client_handle_t ppa_blend_handle, const void *bg_buf,
const void *fg_buf, void *out_buf)
{
/* Background color-keying searches the red ESP32 text. With
* ck_reverse_bg2fg enabled, keyed background pixels are replaced by the
* generated glow foreground while non-key pixels stay unchanged. */
ppa_blend_oper_config_t blend_config = {
.in_bg = {
.buffer = bg_buf,
.pic_w = EXAMPLE_IMAGE_WIDTH,
.pic_h = EXAMPLE_IMAGE_HEIGHT,
.block_w = EXAMPLE_IMAGE_WIDTH,
.block_h = EXAMPLE_IMAGE_HEIGHT,
.block_offset_x = 0,
.block_offset_y = 0,
.blend_cm = PPA_BLEND_COLOR_MODE_RGB565,
},
.in_fg = {
.buffer = fg_buf,
.pic_w = EXAMPLE_IMAGE_WIDTH,
.pic_h = EXAMPLE_IMAGE_HEIGHT,
.block_w = EXAMPLE_IMAGE_WIDTH,
.block_h = EXAMPLE_IMAGE_HEIGHT,
.block_offset_x = 0,
.block_offset_y = 0,
.blend_cm = PPA_BLEND_COLOR_MODE_RGB888,
},
.out = {
.buffer = out_buf,
.buffer_size = EXAMPLE_RGB565_IMAGE_SIZE,
.pic_w = EXAMPLE_IMAGE_WIDTH,
.pic_h = EXAMPLE_IMAGE_HEIGHT,
.block_offset_x = 0,
.block_offset_y = 0,
.blend_cm = PPA_BLEND_COLOR_MODE_RGB565,
},
.bg_alpha_update_mode = PPA_ALPHA_NO_CHANGE,
.fg_alpha_update_mode = PPA_ALPHA_FIX_VALUE,
.fg_alpha_fix_val = 0,
.bg_ck_en = true,
.bg_ck_rgb_low_thres = {
.r = EXAMPLE_RED_KEY_MIN_R,
.g = 0x00,
.b = 0x00,
},
.bg_ck_rgb_high_thres = {
.r = 0xff,
.g = EXAMPLE_RED_KEY_MAX_G,
.b = EXAMPLE_RED_KEY_MAX_B,
},
.ck_reverse_bg2fg = true,
.mode = PPA_TRANS_MODE_BLOCKING,
};
ESP_ERROR_CHECK(ppa_do_blend(ppa_blend_handle, &blend_config));
}
static void blend_glow_around_keyed_text(ppa_client_handle_t ppa_blend_handle, const void *bg_buf,
const void *fg_buf, void *out_buf)
{
/* Background color-keying searches the red ESP32 text. With
* ck_reverse_bg2fg disabled, keyed background pixels stay unchanged while
* all non-key pixels alpha-blend with the generated glow foreground. */
ppa_blend_oper_config_t blend_config = {
.in_bg = {
.buffer = bg_buf,
.pic_w = EXAMPLE_IMAGE_WIDTH,
.pic_h = EXAMPLE_IMAGE_HEIGHT,
.block_w = EXAMPLE_IMAGE_WIDTH,
.block_h = EXAMPLE_IMAGE_HEIGHT,
.block_offset_x = 0,
.block_offset_y = 0,
.blend_cm = PPA_BLEND_COLOR_MODE_RGB565,
},
.in_fg = {
.buffer = fg_buf,
.pic_w = EXAMPLE_IMAGE_WIDTH,
.pic_h = EXAMPLE_IMAGE_HEIGHT,
.block_w = EXAMPLE_IMAGE_WIDTH,
.block_h = EXAMPLE_IMAGE_HEIGHT,
.block_offset_x = 0,
.block_offset_y = 0,
.blend_cm = PPA_BLEND_COLOR_MODE_RGB888,
},
.out = {
.buffer = out_buf,
.buffer_size = EXAMPLE_RGB565_IMAGE_SIZE,
.pic_w = EXAMPLE_IMAGE_WIDTH,
.pic_h = EXAMPLE_IMAGE_HEIGHT,
.block_offset_x = 0,
.block_offset_y = 0,
.blend_cm = PPA_BLEND_COLOR_MODE_RGB565,
},
.bg_alpha_update_mode = PPA_ALPHA_NO_CHANGE,
.fg_alpha_update_mode = PPA_ALPHA_FIX_VALUE,
.fg_alpha_fix_val = EXAMPLE_GLOW_BLEND_ALPHA,
.bg_ck_en = true,
.bg_ck_rgb_low_thres = {
.r = EXAMPLE_RED_KEY_MIN_R,
.g = 0x00,
.b = 0x00,
},
.bg_ck_rgb_high_thres = {
.r = 0xff,
.g = EXAMPLE_RED_KEY_MAX_G,
.b = EXAMPLE_RED_KEY_MAX_B,
},
.ck_reverse_bg2fg = false,
.mode = PPA_TRANS_MODE_BLOCKING,
};
ESP_ERROR_CHECK(ppa_do_blend(ppa_blend_handle, &blend_config));
}
static void encode_and_print_image(const char *effect_name, const uint8_t *image, size_t image_size)
{
/* Binary image data is not safe to print directly on the serial console.
* Base64 turns it into ASCII that pytest can capture and decode. */
size_t encoded_len = 0;
int ret = mbedtls_base64_encode(NULL, 0, &encoded_len, image, image_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);
ESP_ERROR_CHECK(mbedtls_base64_encode(encoded, encoded_len + 1, &encoded_len, image, image_size) == 0 ? ESP_OK : ESP_FAIL);
printf("IMAGE_META effect=%s width=%u height=%u format=RGB565 encoding=base64\n",
effect_name, EXAMPLE_IMAGE_WIDTH, EXAMPLE_IMAGE_HEIGHT);
print_base64_payload(encoded, encoded_len);
free(encoded);
}
void app_main(void)
{
const size_t embedded_size = image_rgb_end - image_rgb_start;
printf("Loading embedded RGB565 image from flash...\n");
printf("Embedded raw image size: %zu bytes\n", embedded_size);
assert(embedded_size == EXAMPLE_RGB565_IMAGE_SIZE);
uint8_t *result_buf = alloc_ppa_buffer(EXAMPLE_RGB565_IMAGE_SIZE);
uint8_t *foreground_buf = alloc_ppa_buffer(EXAMPLE_RGB888_IMAGE_SIZE);
printf("Generating shared glow foreground...\n");
prepare_glow_foreground(foreground_buf);
ppa_client_handle_t ppa_blend_handle = NULL;
ppa_client_config_t ppa_blend_config = {
.oper_type = PPA_OPERATION_BLEND,
.max_pending_trans_num = 1,
};
ESP_ERROR_CHECK(ppa_register_client(&ppa_blend_config, &ppa_blend_handle));
printf("Replacing keyed red text with glow foreground...\n");
replace_keyed_text_with_glow(ppa_blend_handle, image_rgb_start, foreground_buf, result_buf);
encode_and_print_image("replace_keyed_text_with_glow", result_buf, EXAMPLE_RGB565_IMAGE_SIZE);
printf("Blending glow around keyed red text...\n");
blend_glow_around_keyed_text(ppa_blend_handle, image_rgb_start, foreground_buf, result_buf);
encode_and_print_image("blend_glow_around_keyed_text", result_buf, EXAMPLE_RGB565_IMAGE_SIZE);
printf("PPA color key demo done.\n");
ESP_ERROR_CHECK(ppa_unregister_client(ppa_blend_handle));
free(result_buf);
free(foreground_buf);
}

View File

@@ -0,0 +1,205 @@
# SPDX-FileCopyrightText: 2024-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
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
from pytest_embedded_idf.utils import soc_filtered_targets
IMAGE_META_PATTERN = (
r'IMAGE_META effect=(?P<effect>\w+) width=(?P<width>\d+) height=(?P<height>\d+) '
r'format=(?P<format>\w+) encoding=(?P<encoding>\w+)'
)
IMAGE_META_RE = re.compile(IMAGE_META_PATTERN)
IMAGE_CHUNK_PATTERN = r'IMAGE_BASE64 (?P<payload>[A-Za-z0-9+/=]+)'
IMAGE_CHUNK_RE = re.compile(IMAGE_CHUNK_PATTERN)
REPLACE_EFFECT_NAME = 'replace_keyed_text_with_glow'
BLEND_EFFECT_NAME = 'blend_glow_around_keyed_text'
EXPECTED_PIXEL_FORMAT = 'RGB565'
EXPECTED_ENCODING = 'base64'
RGB565_BYTES_PER_PIXEL = 2
RGB888_BYTES_PER_PIXEL = 3
PPM_MAGIC = b'P6'
PPM_MAX_VALUE = b'255'
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 ImageMetadata:
effect: str
width: int
height: int
pixel_format: str
encoding: str
@property
def rgb565_size(self) -> int:
return self.width * self.height * RGB565_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_image_metadata(meta_line: str) -> ImageMetadata:
match = IMAGE_META_RE.fullmatch(meta_line)
if not match:
raise ValueError(f'Invalid image metadata line: {meta_line}')
return ImageMetadata(
effect=match.group('effect'),
width=int(match.group('width')),
height=int(match.group('height')),
pixel_format=match.group('format'),
encoding=match.group('encoding'),
)
def collect_base64_payload(dut: Dut) -> list[str]:
payload_lines: list[str] = []
while True:
match = dut.expect(rf'(?P<line>IMAGE_BASE64_END|{IMAGE_CHUNK_PATTERN}\r?\n)')
line = match.group('line').decode('utf-8').strip()
if line == 'IMAGE_BASE64_END':
return payload_lines
chunk_match = IMAGE_CHUNK_RE.fullmatch(line)
assert chunk_match is not None
payload_lines.append(chunk_match.group('payload'))
def _rgb565_to_rgb888(raw_bytes: bytes) -> bytes:
# PPM stores 8-bit RGB triplets, so expand each RGB565 pixel before writing the artifact.
rgb_bytes = bytearray(len(raw_bytes) // 2 * 3)
for pixel_index, offset in enumerate(range(0, len(raw_bytes), 2)):
pixel = raw_bytes[offset] | (raw_bytes[offset + 1] << 8)
red = (pixel >> 11) & 0x1F
green = (pixel >> 5) & 0x3F
blue = pixel & 0x1F
rgb_offset = pixel_index * 3
rgb_bytes[rgb_offset : rgb_offset + 3] = (
(red << 3) | (red >> 2),
(green << 2) | (green >> 4),
(blue << 3) | (blue >> 2),
)
return bytes(rgb_bytes)
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() :]
expected_size = width * height * RGB888_BYTES_PER_PIXEL
if len(pixel_data) != expected_size:
raise ValueError(f'Expected {expected_size} PPM pixel bytes, got {len(pixel_data)}')
return RgbImage(width=width, height=height, pixels_rgb888=pixel_data)
def decode_rgb565_base64_image(metadata: ImageMetadata, payload_lines: list[str]) -> RgbImage:
if metadata.pixel_format != EXPECTED_PIXEL_FORMAT:
raise ValueError(f'Unsupported pixel format: {metadata.pixel_format}')
if metadata.encoding != EXPECTED_ENCODING:
raise ValueError(f'Unsupported payload encoding: {metadata.encoding}')
raw_bytes = base64.b64decode(''.join(payload_lines), validate=True)
if len(raw_bytes) != metadata.rgb565_size:
raise ValueError(f'Expected {metadata.rgb565_size} decoded bytes, got {len(raw_bytes)}')
return RgbImage(width=metadata.width, height=metadata.height, pixels_rgb888=_rgb565_to_rgb888(raw_bytes))
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 PPA artifact to %s', output_path)
return
logging.info('Saved PPA 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 image 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 effect_output_name(effect: str) -> str:
return f'ppa_color_key_{effect}.ppm'
def effect_golden_name(effect: str) -> str:
return f'golden_{effect}.ppm'
def expect_and_check_effect_image(dut: Dut, effect: str) -> None:
metadata_line = dut.expect(IMAGE_META_PATTERN).group(0).decode('utf-8')
metadata = parse_image_metadata(metadata_line)
assert metadata.effect == effect, f'Expected effect {effect}, got {metadata.effect}'
dut.expect_exact('IMAGE_BASE64_BEGIN')
base64_chunks = collect_base64_payload(dut)
result_image = decode_rgb565_base64_image(metadata, base64_chunks)
output_path = Path(dut.logdir) / effect_output_name(effect)
save_ppm_artifact(result_image, output_path)
assert_image_matches_golden(result_image, Path(__file__).with_name(effect_golden_name(effect)))
@pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_PPA_SUPPORTED == 1'), indirect=['target'])
def test_ppa_color_key(dut: Dut) -> None:
dut.expect_exact('Loading embedded RGB565 image from flash...')
dut.expect(r'Embedded raw image size: \d+ bytes')
dut.expect_exact('Generating shared glow foreground...')
dut.expect_exact('Replacing keyed red text with glow foreground...')
expect_and_check_effect_image(dut, REPLACE_EFFECT_NAME)
dut.expect_exact('Blending glow around keyed red text...')
expect_and_check_effect_image(dut, BLEND_EFFECT_NAME)
dut.expect_exact('PPA color key demo done.')

View File

@@ -0,0 +1 @@
CONFIG_SPIRAM=y