feat(isp): Add example and script to check raw picture by ISP dma input

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
gaoxu
2026-07-28 14:37:45 +08:00
committed by Gao Xu
co-authored by Cursor
parent 2a02c7e55c
commit 5dfcc1bc2b
9 changed files with 542 additions and 85 deletions
+1
View File
@@ -43,6 +43,7 @@ INPUT += \
$(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_demosaic.h \ $(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_demosaic.h \
$(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_sharpen.h \ $(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_sharpen.h \
$(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_core.h \ $(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_core.h \
$(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_dma.h \
$(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_gamma.h \ $(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_gamma.h \
$(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_hist.h \ $(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_hist.h \
$(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_color.h \ $(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_color.h \
+4 -2
View File
@@ -67,6 +67,7 @@ The ISP driver offers following services:
- :ref:`isp-resource-allocation` - covers how to allocate ISP resources with properly set of configurations. It also covers how to recycle the resources when they finished working. - :ref:`isp-resource-allocation` - covers how to allocate ISP resources with properly set of configurations. It also covers how to recycle the resources when they finished working.
- :ref:`isp-enable-disable` - covers how to enable and disable an ISP processor. - :ref:`isp-enable-disable` - covers how to enable and disable an ISP processor.
- :ref:`isp-dma-input` - covers how to feed image frames stored in memory into the ISP through DW-GDMA.
- :ref:`isp-af-statistics` - covers how to get AF statistics one-shot or continuously. - :ref:`isp-af-statistics` - covers how to get AF statistics one-shot or continuously.
- :ref:`isp-awb-statistics` - covers how to get AWB white patches statistics one-shot or continuously. - :ref:`isp-awb-statistics` - covers how to get AWB white patches statistics one-shot or continuously.
- :ref:`isp-ae-statistics` - covers how to get AE statistics one-shot or continuously. - :ref:`isp-ae-statistics` - covers how to get AE statistics one-shot or continuously.
@@ -247,7 +248,7 @@ ISP DMA Input
Besides image streams from camera controllers, the ISP can also read image frames from system memory through DW-GDMA. To use DMA input, set :cpp:member:`esp_isp_processor_cfg_t::input_data_source` in :cpp:type:`esp_isp_processor_cfg_t` to :cpp:enumerator:`ISP_INPUT_DATA_SOURCE_DWGDMA`, and configure the input format, output format, and resolution according to the image frame. Besides image streams from camera controllers, the ISP can also read image frames from system memory through DW-GDMA. To use DMA input, set :cpp:member:`esp_isp_processor_cfg_t::input_data_source` in :cpp:type:`esp_isp_processor_cfg_t` to :cpp:enumerator:`ISP_INPUT_DATA_SOURCE_DWGDMA`, and configure the input format, output format, and resolution according to the image frame.
DMA input is useful for feeding software-generated data, offline RAW images, or other test images in memory into the ISP. It can be used to validate an ISP pipeline without a camera sensor, reproduce issues with a specific input image, or generate inspectable output images in pytest. Call :cpp:func:`esp_isp_dma_process_frame` to send one input buffer to the ISP and write the processed image into an output buffer. The input and output buffers must be accessible by DMA; if cacheable memory is used, perform the required cache synchronization before and after the DMA transfer. DMA input is useful for feeding software-generated data, offline RAW images, or other test images in memory into the ISP. It can be used to validate an ISP pipeline without a camera sensor, reproduce issues with a specific input image. Call :cpp:func:`esp_isp_dma_process_frame` to send one input buffer to the ISP and write the processed image into an output buffer. The input and output buffers must be accessible by DMA; if cacheable memory is used, perform the required cache synchronization before and after the DMA transfer.
ISP AF Controller ISP AF Controller
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
@@ -965,7 +966,7 @@ Application Examples
-------------------- --------------------
* :example:`peripherals/isp/multi_pipelines` demonstrates how to use the ISP pipelines to process the image signals from camera sensors and display the video on LCD screen via DSI peripheral. * :example:`peripherals/isp/multi_pipelines` demonstrates how to use the ISP pipelines to process the image signals from camera sensors and display the video on LCD screen via DSI peripheral.
* :example:`peripherals/isp/dma_input` demonstrates how to feed software-generated RAW8 Bayer data into the ISP through DW-GDMA and save the processed RGB888 frames as PPM images in pytest. * :example:`peripherals/isp/dma_input` demonstrates how to feed a RAW8 BGGR image in memory into the ISP through DW-GDMA. ``pytest_isp_dma_input.py`` saves the processed RGB888 frames as PPM images and compares them pixel by pixel with the checked-in golden image.
* `esp_video/examples <https://github.com/espressif/esp-video-components/tree/master/esp_video/examples>`_ provides some examples of enabling ISP control algorithms. * `esp_video/examples <https://github.com/espressif/esp-video-components/tree/master/esp_video/examples>`_ provides some examples of enabling ISP control algorithms.
API Reference API Reference
@@ -986,5 +987,6 @@ API Reference
.. include-build-file:: inc/isp_color.inc .. include-build-file:: inc/isp_color.inc
.. include-build-file:: inc/isp_crop.inc .. include-build-file:: inc/isp_crop.inc
.. include-build-file:: inc/isp_core.inc .. include-build-file:: inc/isp_core.inc
.. include-build-file:: inc/isp_dma.inc
.. include-build-file:: inc/components/esp_driver_isp/include/driver/isp_types.inc .. include-build-file:: inc/components/esp_driver_isp/include/driver/isp_types.inc
.. include-build-file:: inc/components/hal/include/hal/isp_types.inc .. include-build-file:: inc/components/hal/include/hal/isp_types.inc
+4 -2
View File
@@ -67,6 +67,7 @@ ISP 驱动程序提供以下服务:
- :ref:`isp-resource-allocation` - 涵盖如何通过正确的配置来分配 ISP 资源,以及完成工作后如何回收资源。 - :ref:`isp-resource-allocation` - 涵盖如何通过正确的配置来分配 ISP 资源,以及完成工作后如何回收资源。
- :ref:`isp-enable-disable` - 涵盖如何启用和禁用 ISP 处理器。 - :ref:`isp-enable-disable` - 涵盖如何启用和禁用 ISP 处理器。
- :ref:`isp-dma-input` - 涵盖如何通过 DW-GDMA 将存储在内存中的图像帧送入 ISP。
- :ref:`isp-af-statistics` - 涵盖如何单次或连续获取 AF 统计信息。 - :ref:`isp-af-statistics` - 涵盖如何单次或连续获取 AF 统计信息。
- :ref:`isp-awb-statistics` - 涵盖如何单次或连续获取 AWB 白块统计信息。 - :ref:`isp-awb-statistics` - 涵盖如何单次或连续获取 AWB 白块统计信息。
- :ref:`isp-ae-statistics` - 涵盖如何单次或连续获取 AE 统计信息。 - :ref:`isp-ae-statistics` - 涵盖如何单次或连续获取 AE 统计信息。
@@ -247,7 +248,7 @@ ISP DMA 输入
除来自摄像头控制器的数据流外,ISP 还可以通过 DW-GDMA 从系统存储中读取图像帧作为输入。使用 DMA 输入时,应在 :cpp:type:`esp_isp_processor_cfg_t` 中将 :cpp:member:`esp_isp_processor_cfg_t::input_data_source` 配置为 :cpp:enumerator:`ISP_INPUT_DATA_SOURCE_DWGDMA`,并根据输入图像格式设置输入、输出格式及分辨率。 除来自摄像头控制器的数据流外,ISP 还可以通过 DW-GDMA 从系统存储中读取图像帧作为输入。使用 DMA 输入时,应在 :cpp:type:`esp_isp_processor_cfg_t` 中将 :cpp:member:`esp_isp_processor_cfg_t::input_data_source` 配置为 :cpp:enumerator:`ISP_INPUT_DATA_SOURCE_DWGDMA`,并根据输入图像格式设置输入、输出格式及分辨率。
DMA 输入适用于将软件生成的数据、离线保存的 RAW 图像或其他内存中的测试图像送入 ISP 进行处理。它可用于无摄像头传感器参与时验证 ISP 流水线、复现特定输入图像的问题,或在 pytest 中生成可检查的输出图像。调用 :cpp:func:`esp_isp_dma_process_frame` 可以将一帧输入缓冲区送入 ISP,并将处理后的图像写入输出缓冲区。输入和输出缓冲区需要满足 DMA 访问要求;若使用带 cache 的内存,请在 DMA 传输前后执行必要的 cache 同步。 DMA 输入适用于将软件生成的数据、离线保存的 RAW 图像或其他内存中的测试图像送入 ISP 进行处理。它可用于无摄像头传感器参与时验证 ISP 流水线、复现特定输入图像的问题。调用 :cpp:func:`esp_isp_dma_process_frame` 可以将一帧输入缓冲区送入 ISP,并将处理后的图像写入输出缓冲区。输入和输出缓冲区需要满足 DMA 访问要求;若使用带 cache 的内存,请在 DMA 传输前后执行必要的 cache 同步。
ISP AF 控制器 ISP AF 控制器
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
@@ -965,7 +966,7 @@ Kconfig 选项 :ref:`CONFIG_ISP_CTRL_FUNC_IN_IRAM` 支持:
-------- --------
* :example:`peripherals/isp/multi_pipelines` 演示了如何使用 ISP 流水线处理来自摄像头传感器的图像信号,并通过 DSI 外设在 LCD 屏幕上显示视频。 * :example:`peripherals/isp/multi_pipelines` 演示了如何使用 ISP 流水线处理来自摄像头传感器的图像信号,并通过 DSI 外设在 LCD 屏幕上显示视频。
* :example:`peripherals/isp/dma_input` 演示了如何通过 DW-GDMA 将软件生成的 RAW8 Bayer 数据送入 ISP,并在 pytest 中将处理后的 RGB888 帧保存为 PPM 图片。 * :example:`peripherals/isp/dma_input` 演示了如何通过 DW-GDMA 将内存中的 RAW8 BGGR 图像送入 ISP。``pytest_isp_dma_input.py`` 会将处理后的 RGB888 帧保存为 PPM 图片,并与示例中提交的 golden 图片进行逐像素比较。
* `esp_video/examples <https://github.com/espressif/esp-video-components/tree/master/esp_video/examples>`_ 中包含自动启用 ISP 控制算法的一些示例。 * `esp_video/examples <https://github.com/espressif/esp-video-components/tree/master/esp_video/examples>`_ 中包含自动启用 ISP 控制算法的一些示例。
API 参考 API 参考
@@ -986,5 +987,6 @@ API 参考
.. include-build-file:: inc/isp_color.inc .. include-build-file:: inc/isp_color.inc
.. include-build-file:: inc/isp_crop.inc .. include-build-file:: inc/isp_crop.inc
.. include-build-file:: inc/isp_core.inc .. include-build-file:: inc/isp_core.inc
.. include-build-file:: inc/isp_dma.inc
.. include-build-file:: inc/components/esp_driver_isp/include/driver/isp_types.inc .. include-build-file:: inc/components/esp_driver_isp/include/driver/isp_types.inc
.. include-build-file:: inc/components/hal/include/hal/isp_types.inc .. include-build-file:: inc/components/hal/include/hal/isp_types.inc
+7 -11
View File
@@ -5,22 +5,20 @@
## Overview ## Overview
This example generates a standard RAW8 Bayer color-bar pattern in software, writes it into a DMA-capable PSRAM input buffer, feeds it into the ISP through DW-GDMA, applies the ISP color adjustment module, and prints the RGB888 output as base64. The pytest script decodes the output into PPM images for inspection. This example embeds a 240 x 280 RAW8 Bayer image of a real scene in flash, copies it into a DMA-capable PSRAM input buffer, feeds it into the ISP through DW-GDMA, and prints the RGB888 output as base64. The pytest script decodes the output into a PPM image and compares it with the checked-in golden image.
The data flow is: The data flow is:
1. A synthetic RAW8 Bayer **color-bar** pattern is generated in software into the ISP DMA input buffer. 1. The embedded BGGR RAW8 image is copied from flash into the ISP DMA input buffer.
2. The pattern is transferred into the ISP via `DW-GDMA → ISP DMA input`. 2. The image is transferred into the ISP via `DW-GDMA → ISP DMA input`.
3. The ISP processes the data (demosaic, color adjustment) and outputs RGB888 (BGR24 byte layout). 3. The ISP processes the data (demosaic, color adjustment) and outputs RGB888 (BGR24 byte layout).
4. The RGB888 frame is base64-encoded and printed with machine-parseable markers. 4. The RGB888 frame is base64-encoded and printed with machine-parseable markers.
5. pytest decodes the payload, swaps BGR→RGB, and saves one PPM file per frame. 5. pytest decodes the payload, swaps BGR→RGB, saves one PPM file per frame, and compares it with the golden image.
## Hardware Required ## Hardware Required
- An ESP32-P4 devkit with PSRAM (this example allocates the ISP DMA input/output buffers from PSRAM). - An ESP32-P4 devkit with PSRAM (this example allocates the ISP DMA input/output buffers from PSRAM).
If you replace the software-generated pattern with a RAW image embedded in flash, copy it into a DMA-capable buffer before feeding it to the ISP DMA input path.
## How to Use ## How to Use
Run the test locally. It builds and flashes the example, captures the serial output, and saves one PPM artifact per frame: Run the test locally. It builds and flashes the example, captures the serial output, and saves one PPM artifact per frame:
@@ -30,17 +28,15 @@ cd examples/peripherals/isp/dma_input
pytest pytest_isp_dma_input.py --target esp32p4 --port PORT pytest pytest_isp_dma_input.py --target esp32p4 --port PORT
``` ```
The pytest log directory (`dut.logdir`) contains `reference.ppm` for the standard reference color-bar image and `isp_dma_input_frame00.ppm`, ... for the decoded ISP output frames. The pytest script checks that the decoded frame contains the expected standard color-bar structure and verifies that the ISP color brightness adjustment brightens the black bar. The pytest log directory (`dut.logdir`) contains `isp_dma_input_frame00.ppm`, the decoded RGB888 ISP output from the current hardware run. The repository includes [golden/golden.ppm](golden/golden.ppm), the checked-in reference image. The test compares the decoded RGB888 pixels with this reference image, making image-quality regressions visible in review and detectable in CI.
ISP feature-specific setup is kept under `main/isp_features/<feature>/`, and pytest feature checks are kept under `pytest_features/<feature>.py`. The current color brightness check lives in `main/isp_features/color/` and `pytest_features/color_brightness.py`; new ISP feature checks can follow the same pattern while reusing the common DW-GDMA input/output and image parsing flow.
## Example Output ## Example Output
Each frame uses the same standard vertical color-bar input pattern with positive color brightness enabled to validate the ISP color adjustment module. The example processes one embedded 240 x 280 BGGR RAW8 frame.
```text ```text
Feeding 1 frames through ISP DMA input... Feeding 1 frames through ISP DMA input...
IMAGE_META frame=0 width=128 height=96 format=BGR24 encoding=base64 color_brightness=64 IMAGE_META frame=0 width=240 height=280 format=BGR24 encoding=base64
IMAGE_BASE64_BEGIN IMAGE_BASE64_BEGIN
IMAGE_BASE64 ... IMAGE_BASE64 ...
IMAGE_BASE64_END IMAGE_BASE64_END
Binary file not shown.
@@ -1,3 +1,7 @@
idf_component_register(SRCS "isp_dma_main.c" idf_component_register(SRCS "isp_dma_main.c"
PRIV_REQUIRES esp_driver_isp esp_mm esp_psram mbedtls PRIV_REQUIRES esp_driver_isp esp_mm esp_psram mbedtls
INCLUDE_DIRS ".") INCLUDE_DIRS ".")
target_add_binary_data(${COMPONENT_LIB}
"${CMAKE_CURRENT_LIST_DIR}/assets/sensor_240x280_bggr.raw"
BINARY RENAME_TO "sensor_raw")
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdbool.h> #include <string.h>
#include <assert.h> #include <assert.h>
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
@@ -19,42 +19,15 @@
#include "driver/isp_core.h" #include "driver/isp_core.h"
#include "driver/isp_color.h" #include "driver/isp_color.h"
#define EXAMPLE_WIDTH 128 #define EXAMPLE_WIDTH 240
#define EXAMPLE_HEIGHT 96 #define EXAMPLE_HEIGHT 280
#define EXAMPLE_BASE64_CHUNK_LEN 384 #define EXAMPLE_BASE64_CHUNK_LEN 384
#define EXAMPLE_BASE64_DELAY_MS 10
#define EXAMPLE_DMA_ALIGN 64 #define EXAMPLE_DMA_ALIGN 64
#define EXAMPLE_FRAME_COUNT 2 #define EXAMPLE_FRAME_COUNT 1
static const uint8_t s_color_bars[8][3] = { extern const uint8_t sensor_raw_start[] asm("_binary_sensor_raw_start");
{255, 255, 255}, // white extern const uint8_t sensor_raw_end[] asm("_binary_sensor_raw_end");
{255, 255, 0}, // yellow
{ 0, 255, 255}, // cyan
{ 0, 255, 0}, // green
{255, 0, 255}, // magenta
{255, 0, 0}, // red
{ 0, 0, 255}, // blue
{ 0, 0, 0}, // black
};
static void s_generate_raw8_color_bars(uint8_t *raw, uint32_t w, uint32_t h)
{
for (uint32_t y = 0; y < h; y++) {
bool even_row = ((y & 1) == 0);
for (uint32_t x = 0; x < w; x++) {
uint32_t bar = (x * 8) / w;
uint8_t r = s_color_bars[bar][0];
uint8_t g = s_color_bars[bar][1];
uint8_t b = s_color_bars[bar][2];
bool even_col = ((x & 1) == 0);
if (even_row) {
raw[y * w + x] = even_col ? b : g;
} else {
raw[y * w + x] = even_col ? g : r;
}
}
}
}
static void *s_alloc_dma_buffer(size_t size) static void *s_alloc_dma_buffer(size_t size)
{ {
@@ -86,7 +59,7 @@ static void s_print_base64_payload(const unsigned char *encoded, size_t encoded_
} }
printf("IMAGE_BASE64 %.*s\n", (int)chunk_len, (const char *)&encoded[offset]); printf("IMAGE_BASE64 %.*s\n", (int)chunk_len, (const char *)&encoded[offset]);
fflush(stdout); fflush(stdout);
vTaskDelay(pdMS_TO_TICKS(1)); vTaskDelay(pdMS_TO_TICKS(EXAMPLE_BASE64_DELAY_MS));
} }
printf("IMAGE_BASE64_END\n"); printf("IMAGE_BASE64_END\n");
fflush(stdout); fflush(stdout);
@@ -120,6 +93,10 @@ void app_main(void)
uint8_t *isp_out_buf = s_alloc_dma_buffer(out_size); uint8_t *isp_out_buf = s_alloc_dma_buffer(out_size);
assert(isp_in_buf && isp_out_buf); assert(isp_in_buf && isp_out_buf);
size_t embedded_raw_size = sensor_raw_end - sensor_raw_start;
assert(embedded_raw_size == in_size);
memcpy(isp_in_buf, sensor_raw_start, embedded_raw_size);
size_t encoded_len = 0; size_t encoded_len = 0;
int ret = mbedtls_base64_encode(NULL, 0, &encoded_len, isp_out_buf, out_size); int ret = mbedtls_base64_encode(NULL, 0, &encoded_len, isp_out_buf, out_size);
ESP_ERROR_CHECK((ret == MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) ? ESP_OK : ESP_FAIL); ESP_ERROR_CHECK((ret == MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) ? ESP_OK : ESP_FAIL);
@@ -128,8 +105,6 @@ void app_main(void)
printf("Feeding %d frames through ISP DMA input...\n", EXAMPLE_FRAME_COUNT); printf("Feeding %d frames through ISP DMA input...\n", EXAMPLE_FRAME_COUNT);
for (int frame = 0; frame < EXAMPLE_FRAME_COUNT; frame++) { for (int frame = 0; frame < EXAMPLE_FRAME_COUNT; frame++) {
s_generate_raw8_color_bars(isp_in_buf, h_res, v_res);
ESP_ERROR_CHECK(esp_cache_msync(isp_in_buf, in_size, ESP_CACHE_MSYNC_FLAG_DIR_C2M)); ESP_ERROR_CHECK(esp_cache_msync(isp_in_buf, in_size, ESP_CACHE_MSYNC_FLAG_DIR_C2M));
ESP_ERROR_CHECK(esp_isp_dma_process_frame(isp_proc, isp_out_buf, isp_in_buf, 1000)); ESP_ERROR_CHECK(esp_isp_dma_process_frame(isp_proc, isp_out_buf, isp_in_buf, 1000));
ESP_ERROR_CHECK(esp_cache_msync(isp_out_buf, out_size, ESP_CACHE_MSYNC_FLAG_DIR_M2C)); ESP_ERROR_CHECK(esp_cache_msync(isp_out_buf, out_size, ESP_CACHE_MSYNC_FLAG_DIR_M2C));
@@ -6,6 +6,7 @@ import logging
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import List
import pytest import pytest
from pytest_embedded import Dut from pytest_embedded import Dut
@@ -22,19 +23,9 @@ IMAGE_CHUNK_PATTERN = (
r'(?=\r?\n|IMAGE_BASE64|IMAGE_BASE64_END|Frame )' r'(?=\r?\n|IMAGE_BASE64|IMAGE_BASE64_END|Frame )'
) )
IMAGE_OUTPUT_TEMPLATE = 'isp_dma_input_frame{frame:02d}.ppm' IMAGE_OUTPUT_TEMPLATE = 'isp_dma_input_frame{frame:02d}.ppm'
REFERENCE_IMAGE_NAME = 'reference.ppm' REFERENCE_IMAGE_PATH = Path(__file__).parent / 'golden' / 'golden.ppm'
EXPECTED_PIXEL_FORMAT = 'BGR24' EXPECTED_PIXEL_FORMAT = 'BGR24'
EXPECTED_ENCODING = 'base64' EXPECTED_ENCODING = 'base64'
STANDARD_COLOR_BARS_RGB888 = (
(255, 255, 255), # white
(255, 255, 0), # yellow
(0, 255, 255), # cyan
(0, 255, 0), # green
(255, 0, 255), # magenta
(255, 0, 0), # red
(0, 0, 255), # blue
(0, 0, 0), # black
)
RGB888_BYTES_PER_PIXEL = 3 RGB888_BYTES_PER_PIXEL = 3
PPM_MAGIC = b'P6' PPM_MAGIC = b'P6'
PPM_MAX_VALUE = b'255' PPM_MAX_VALUE = b'255'
@@ -75,8 +66,8 @@ def parse_image_metadata(meta_line: str) -> ImageMetadata:
) )
def collect_base64_payload(dut: Dut) -> list[str]: def collect_base64_payload(dut: Dut) -> List[str]:
payload_lines: list[str] = [] payload_lines: List[str] = []
while True: while True:
match = dut.expect(rf'IMAGE_BASE64_END|{IMAGE_CHUNK_PATTERN}', timeout=60) match = dut.expect(rf'IMAGE_BASE64_END|{IMAGE_CHUNK_PATTERN}', timeout=60)
if match.group(0).decode('utf-8') == 'IMAGE_BASE64_END': if match.group(0).decode('utf-8') == 'IMAGE_BASE64_END':
@@ -98,7 +89,7 @@ def _encode_ppm(image: RgbImage) -> bytes:
return header + image.pixels_rgb888 return header + image.pixels_rgb888
def decode_bgr24_base64_image(metadata: ImageMetadata, payload_lines: list[str]) -> RgbImage: def decode_bgr24_base64_image(metadata: ImageMetadata, payload_lines: List[str]) -> RgbImage:
if metadata.pixel_format != EXPECTED_PIXEL_FORMAT: if metadata.pixel_format != EXPECTED_PIXEL_FORMAT:
raise ValueError(f'Unsupported pixel format: {metadata.pixel_format}') raise ValueError(f'Unsupported pixel format: {metadata.pixel_format}')
if metadata.encoding != EXPECTED_ENCODING: if metadata.encoding != EXPECTED_ENCODING:
@@ -112,17 +103,6 @@ def decode_bgr24_base64_image(metadata: ImageMetadata, payload_lines: list[str])
return RgbImage(width=metadata.width, height=metadata.height, pixels_rgb888=_bgr24_to_rgb888(raw_bytes)) return RgbImage(width=metadata.width, height=metadata.height, pixels_rgb888=_bgr24_to_rgb888(raw_bytes))
def generate_standard_color_bar_image(width: int, height: int) -> RgbImage:
pixels = bytearray(width * height * RGB888_BYTES_PER_PIXEL)
for y in range(height):
for x in range(width):
bar_index = (x * len(STANDARD_COLOR_BARS_RGB888)) // width
offset = (y * width + x) * RGB888_BYTES_PER_PIXEL
pixels[offset : offset + RGB888_BYTES_PER_PIXEL] = STANDARD_COLOR_BARS_RGB888[bar_index]
return RgbImage(width=width, height=height, pixels_rgb888=bytes(pixels))
def save_ppm_artifact(image: RgbImage, output_path: Path) -> None: def save_ppm_artifact(image: RgbImage, output_path: Path) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True) output_path.parent.mkdir(parents=True, exist_ok=True)
try: try:
@@ -134,14 +114,45 @@ def save_ppm_artifact(image: RgbImage, output_path: Path) -> None:
logging.info('Saved ISP DMA artifact to %s', output_path) logging.info('Saved ISP DMA artifact to %s', output_path)
def assert_image_is_meaningful(image: RgbImage) -> None: def load_ppm_image(image_path: Path) -> RgbImage:
distinct_pixels = {image.pixels_rgb888[i : i + 3] for i in range(0, len(image.pixels_rgb888), 3)} ppm_data = image_path.read_bytes()
assert len(distinct_pixels) > 1, 'ISP output is a single flat color; the pipeline likely produced no real data' try:
magic, dimensions, max_value, pixels_rgb888 = ppm_data.split(b'\n', 3)
width, height = (int(value) for value in dimensions.split())
except ValueError as error:
raise ValueError(f'Invalid PPM image: {image_path}') from error
if magic != PPM_MAGIC or max_value != PPM_MAX_VALUE:
raise ValueError(f'Unsupported PPM image: {image_path}')
return RgbImage(width=width, height=height, pixels_rgb888=pixels_rgb888)
def assert_image_matches_reference(result_image: RgbImage, reference_image: RgbImage) -> None:
assert (result_image.width, result_image.height) == (reference_image.width, reference_image.height), (
f'ISP output dimensions do not match reference image: '
f'{result_image.width}x{result_image.height} != {reference_image.width}x{reference_image.height}'
)
if result_image.pixels_rgb888 == reference_image.pixels_rgb888:
return
mismatch_offset = next(
offset
for offset, (actual, expected) in enumerate(zip(result_image.pixels_rgb888, reference_image.pixels_rgb888))
if actual != expected
)
pixel_index, channel = divmod(mismatch_offset, RGB888_BYTES_PER_PIXEL)
raise AssertionError(
f'ISP output does not match reference image at pixel {pixel_index}, channel {channel}: '
f'{result_image.pixels_rgb888[mismatch_offset]} != {reference_image.pixels_rgb888[mismatch_offset]}'
)
@pytest.mark.generic @pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_ISP_SUPPORTED == 1'), indirect=['target']) @idf_parametrize('target', soc_filtered_targets('SOC_ISP_SUPPORTED == 1'), indirect=['target'])
def test_isp_dma_input_example(dut: Dut) -> None: def test_isp_dma_input_example(dut: Dut) -> None:
reference_image = load_ppm_image(REFERENCE_IMAGE_PATH)
frame_count_match = dut.expect(r'Feeding (?P<frame_count>\d+) frames through ISP DMA input...') frame_count_match = dut.expect(r'Feeding (?P<frame_count>\d+) frames through ISP DMA input...')
expected_frame_count = int(frame_count_match.group('frame_count').decode('utf-8')) expected_frame_count = int(frame_count_match.group('frame_count').decode('utf-8'))
logging.info('Expecting %d ISP DMA frame(s)', expected_frame_count) logging.info('Expecting %d ISP DMA frame(s)', expected_frame_count)
@@ -156,10 +167,6 @@ def test_isp_dma_input_example(dut: Dut) -> None:
metadata.height, metadata.height,
metadata.pixel_format, metadata.pixel_format,
) )
if expected_frame == 0:
reference_image = generate_standard_color_bar_image(metadata.width, metadata.height)
save_ppm_artifact(reference_image, Path(dut.logdir) / REFERENCE_IMAGE_NAME)
dut.expect_exact('IMAGE_BASE64_BEGIN') dut.expect_exact('IMAGE_BASE64_BEGIN')
logging.info('Receiving base64 image payload for frame %d', metadata.frame) logging.info('Receiving base64 image payload for frame %d', metadata.frame)
payload_lines = collect_base64_payload(dut) payload_lines = collect_base64_payload(dut)
@@ -169,7 +176,7 @@ def test_isp_dma_input_example(dut: Dut) -> None:
logging.info('Decoded frame %d to RGB888 image', metadata.frame) logging.info('Decoded frame %d to RGB888 image', metadata.frame)
output_path = Path(dut.logdir) / IMAGE_OUTPUT_TEMPLATE.format(frame=metadata.frame) output_path = Path(dut.logdir) / IMAGE_OUTPUT_TEMPLATE.format(frame=metadata.frame)
save_ppm_artifact(result_image, output_path) save_ppm_artifact(result_image, output_path)
assert_image_is_meaningful(result_image) assert_image_matches_reference(result_image, reference_image)
dut.expect_exact(f'Frame {expected_frame} done') dut.expect_exact(f'Frame {expected_frame} done')
dut.expect_exact('ISP DMA visual demo done.') dut.expect_exact('ISP DMA visual demo done.')