diff --git a/app/common/CMakeLists.txt b/app/common/CMakeLists.txt index a93db54..40748b2 100644 --- a/app/common/CMakeLists.txt +++ b/app/common/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory_ifdef(CONFIG_APP_WATCHDOG init/watchdog) add_subdirectory_ifdef(CONFIG_APP_AUDIO_SELCALL modules/audio_selcall) add_subdirectory_ifdef(CONFIG_APP_BUTTON modules/button) +add_subdirectory_ifdef(CONFIG_APP_CAPTURE_TESTPATTERN modules/capture_testpattern) add_subdirectory_ifdef(CONFIG_APP_DAC_SAWTOOTH modules/dac_sawtooth) add_subdirectory_ifdef(CONFIG_APP_HEARTBEAT modules/heartbeat) add_subdirectory_ifdef(CONFIG_APP_LED_ROLLOVER modules/led_rollover) diff --git a/app/common/Kconfig b/app/common/Kconfig index 1900052..76d2371 100644 --- a/app/common/Kconfig +++ b/app/common/Kconfig @@ -25,12 +25,20 @@ config APP_AUDIO_SELCALL default y depends on I2C depends on AUDIO_CODEC + depends on !PHYTEC_SOM_TESTING config APP_BUTTON bool "Enable the button example." default y depends on GPIO depends on !PHYTEC_KIT_TESTING + depends on !PHYTEC_SOM_TESTING + +config APP_CAPTURE_TESTPATTERN + bool "Enable the capture testcard example." + default y + depends on VIDEO + depends on PHYTEC_SOM_TESTING || PHYTEC_KIT_TESTING config APP_DAC_SAWTOOTH bool "Enable the DAC sawtooth example." @@ -45,6 +53,7 @@ config APP_HEARTBEAT default y depends on GPIO depends on !APP_LED_ROLLOVER + depends on !PHYTEC_SOM_TESTING config APP_LED_ROLLOVER bool "Enable the LED rollover example using LED subsystem." diff --git a/app/common/modules/capture_testpattern/CMakeLists.txt b/app/common/modules/capture_testpattern/CMakeLists.txt new file mode 100644 index 0000000..8579e46 --- /dev/null +++ b/app/common/modules/capture_testpattern/CMakeLists.txt @@ -0,0 +1,2 @@ +target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/capture_testpattern.c) +target_include_directories(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../) diff --git a/app/common/modules/capture_testpattern/Kconfig b/app/common/modules/capture_testpattern/Kconfig new file mode 100644 index 0000000..5f832b7 --- /dev/null +++ b/app/common/modules/capture_testpattern/Kconfig @@ -0,0 +1,28 @@ +# VIDEO resolution settings + +# Copyright (c) 2025 PHYTEC +# SPDX-License-Identifier: Apache-2.0 + +mainmenu "Video capture to LVGL sample application" + +menu "Video capture configuration" + +config VIDEO_WIDTH + int "Define the width of the video" + default 320 + +config VIDEO_HEIGHT + int "Define the height of the video" + default 240 + +config VIDEO_HFLIP + bool "Horizontal flip" + default n + +config VIDEO_VFLIP + bool "Vertical flip" + default n + +endmenu + +source "Kconfig.zephyr" diff --git a/app/common/modules/capture_testpattern/capture_testpattern.c b/app/common/modules/capture_testpattern/capture_testpattern.c new file mode 100644 index 0000000..174427c --- /dev/null +++ b/app/common/modules/capture_testpattern/capture_testpattern.c @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2025 PHYTEC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include +#include + +#include + +#include +LOG_MODULE_REGISTER(capture_testpattern, CONFIG_APP_LOG_LEVEL); + +#include "check_test_pattern.h" + +#define CONFIG_VIDEO_WIDTH 640 +#define CONFIG_VIDEO_HEIGHT 480 + +/* size of stack area used by each thread */ +#define CAPTURE_TESTPATTERN_STACKSIZE 8192 +/* scheduling priority used by each thread */ +#define CAPTURE_TESTPATTERN_PRIORITY 5 + + +int capture_testpattern(void) +{ + struct video_buffer *buffers[2]; + struct video_buffer *vbuf = &(struct video_buffer){}; + const struct device *display_dev; + struct video_format fmt; + struct video_caps caps; + struct video_frmival frmival; + struct video_frmival_enum fie; + unsigned int frame = 0; + const struct device *video_dev; + size_t bsize; + int i = 0; + int err; + + display_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display)); + if (!device_is_ready(display_dev)) { + LOG_ERR("Device not ready, aborting test"); + return 0; + } + + + + video_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_camera)); + if (!device_is_ready(video_dev)) { + LOG_ERR("%s device is not ready", video_dev->name); + return 0; + } + + LOG_INF("- Device name: %s", video_dev->name); + + /* Get capabilities */ + if (video_get_caps(video_dev, VIDEO_EP_OUT, &caps)) { + LOG_ERR("Unable to retrieve video capabilities"); + return 0; + } + + LOG_INF("- Capabilities:"); + while (caps.format_caps[i].pixelformat) { + const struct video_format_cap *fcap = &caps.format_caps[i]; + /* four %c to string */ + LOG_INF(" %c%c%c%c width [%u; %u; %u] height [%u; %u; %u]", + (char)fcap->pixelformat, (char)(fcap->pixelformat >> 8), + (char)(fcap->pixelformat >> 16), (char)(fcap->pixelformat >> 24), + fcap->width_min, fcap->width_max, fcap->width_step, fcap->height_min, + fcap->height_max, fcap->height_step); + i++; + } + + /* Get default/native format */ + if (video_get_format(video_dev, VIDEO_EP_OUT, &fmt)) { + LOG_ERR("Unable to retrieve video format"); + return 0; + } + + /* Set format */ + fmt.width = CONFIG_VIDEO_WIDTH; + fmt.height = CONFIG_VIDEO_HEIGHT; + fmt.pitch = fmt.width * 2; + + if (video_set_format(video_dev, VIDEO_EP_OUT, &fmt)) { + LOG_ERR("Unable to set up video format"); + return 0; + } + + LOG_INF("- Format: %c%c%c%c %ux%u %u", (char)fmt.pixelformat, (char)(fmt.pixelformat >> 8), + (char)(fmt.pixelformat >> 16), (char)(fmt.pixelformat >> 24), fmt.width, fmt.height, + fmt.pitch); + + if (!video_get_frmival(video_dev, VIDEO_EP_OUT, &frmival)) { + LOG_INF("- Default frame rate : %f fps", + 1.0 * frmival.denominator / frmival.numerator); + } + + LOG_INF("- Supported frame intervals for the default format:"); + memset(&fie, 0, sizeof(fie)); + fie.format = &fmt; + while (video_enum_frmival(video_dev, VIDEO_EP_OUT, &fie) == 0) { + if (fie.type == VIDEO_FRMIVAL_TYPE_DISCRETE) { + LOG_INF(" %u/%u ", fie.discrete.numerator, fie.discrete.denominator); + } else { + LOG_INF(" [min = %u/%u; max = %u/%u; step = %u/%u]", + fie.stepwise.min.numerator, fie.stepwise.min.denominator, + fie.stepwise.max.numerator, fie.stepwise.max.denominator, + fie.stepwise.step.numerator, fie.stepwise.step.denominator); + } + fie.index++; + } + + /* Set controls */ + if (IS_ENABLED(CONFIG_VIDEO_CTRL_HFLIP)) { + video_set_ctrl(video_dev, VIDEO_CID_HFLIP, (void *)1); + } + + if (IS_ENABLED(CONFIG_VIDEO_CTRL_VFLIP)) { + video_set_ctrl(video_dev, VIDEO_CID_VFLIP, (void *)1); + } + + video_set_ctrl(video_dev, VIDEO_CID_TEST_PATTERN, (void *)1); + + /* Size to allocate for each buffer */ + if (caps.min_line_count == LINE_COUNT_HEIGHT) { + bsize = fmt.pitch * fmt.height; + } else { + bsize = fmt.pitch * caps.min_line_count; + } + + /* Alloc video buffers and enqueue for capture */ + for (i = 0; i < ARRAY_SIZE(buffers); i++) { + /* + * For some hardwares, such as the PxP used on i.MX RT1170 to do image rotation, + * buffer alignment is needed in order to achieve the best performance + */ + buffers[i] = video_buffer_aligned_alloc(bsize, CONFIG_VIDEO_BUFFER_POOL_ALIGN, + K_FOREVER); + if (buffers[i] == NULL) { + LOG_ERR("Unable to alloc video buffer"); + return 0; + } + + video_enqueue(video_dev, VIDEO_EP_OUT, buffers[i]); + } + + /* Start video capture */ + if (video_stream_start(video_dev)) { + LOG_ERR("Unable to start capture (interface)"); + return 0; + } + + display_blanking_off(display_dev); + + const lv_img_dsc_t video_img = { + .header.w = CONFIG_VIDEO_WIDTH, + .header.h = CONFIG_VIDEO_HEIGHT, + .data_size = CONFIG_VIDEO_WIDTH * CONFIG_VIDEO_HEIGHT * sizeof(lv_color_t), + .header.cf = LV_COLOR_FORMAT_NATIVE, + .data = (const uint8_t *)buffers[0]->buffer, + }; + + lv_obj_t *screen = lv_img_create(lv_scr_act()); + + LOG_INF("- Capture started"); + + while (1) { + + err = video_dequeue(video_dev, VIDEO_EP_OUT, &vbuf, K_FOREVER); + if (err) { + LOG_ERR("Unable to dequeue video buf"); + return 0; + } + + LOG_DBG("Got frame %u! size: %u; timestamp %u ms", frame++, vbuf->bytesused, + vbuf->timestamp); + + lv_img_set_src(screen, &video_img); + lv_obj_align(screen, LV_ALIGN_BOTTOM_LEFT, 0, 0); + + lv_task_handler(); + + err = video_enqueue(video_dev, VIDEO_EP_OUT, vbuf); + if (err) { + LOG_ERR("Unable to requeue video buf"); + return 0; + } + } +} + +K_THREAD_DEFINE(capture_testpattern_tid, CAPTURE_TESTPATTERN_STACKSIZE, + capture_testpattern, NULL, NULL, NULL, + CAPTURE_TESTPATTERN_PRIORITY, 0, 0); diff --git a/app/common/modules/capture_testpattern/check_test_pattern.h b/app/common/modules/capture_testpattern/check_test_pattern.h new file mode 100644 index 0000000..765f819 --- /dev/null +++ b/app/common/modules/capture_testpattern/check_test_pattern.h @@ -0,0 +1,154 @@ +/* + * Copyright 2025 PHYTEC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef TEST_PATTERN_CHECK_H_ +#define TEST_PATTERN_CHECK_H_ + +#include + +#include + +#define LAB_THRESHOLD 10.0 + +#define BARS_NUM 8 +#define PIXELS_NUM 5 + +typedef struct { + double L; + double a; + double b; +} CIELAB; + +/* + * This is measured on a real 8-colorbar pattern generated by an ov5640 camera sensor. + * For other sensors, it can be slightly different. If it doesn't fit anymore, either + * this array or the LAB_THRESHOLD can be modified. + * + * {White, Yellow, Cyan, Green, Magenta, Red, Blue, Black} + */ +static const CIELAB colorbars_target[] = { + {100.0, 0.0053, -0.0104}, {97.1804, -21.2151, 91.3538}, {90.1352, -58.4675, 6.0570}, + {87.7630, -85.9469, 83.2128}, {56.6641, 95.0182, -66.9129}, {46.6937, 72.7494, 49.5801}, + {27.6487, 71.5662, -97.4712}, {1.3726, -2.8040, 2.0043}}; + +static inline CIELAB rgb888_to_lab(const uint8_t r, const uint8_t g, const uint8_t b) +{ + CIELAB lab; + + double r_lin = r / 255.0; + double g_lin = g / 255.0; + double b_lin = b / 255.0; + + r_lin = r_lin > 0.04045 ? pow((r_lin + 0.055) / 1.055, 2.4) : r_lin / 12.92; + g_lin = g_lin > 0.04045 ? pow((g_lin + 0.055) / 1.055, 2.4) : g_lin / 12.92; + b_lin = b_lin > 0.04045 ? pow((b_lin + 0.055) / 1.055, 2.4) : b_lin / 12.92; + + double x = r_lin * 0.4124 + g_lin * 0.3576 + b_lin * 0.1805; + double y = r_lin * 0.2126 + g_lin * 0.7152 + b_lin * 0.0722; + double z = r_lin * 0.0193 + g_lin * 0.1192 + b_lin * 0.9505; + + x /= 0.95047; + z /= 1.08883; + + x = x > 0.008856 ? pow(x, 1.0 / 3.0) : (7.787 * x) + (16.0 / 116.0); + y = y > 0.008856 ? pow(y, 1.0 / 3.0) : (7.787 * y) + (16.0 / 116.0); + z = z > 0.008856 ? pow(z, 1.0 / 3.0) : (7.787 * z) + (16.0 / 116.0); + + lab.L = 116.0 * y - 16.0; + lab.a = 500.0 * (x - y); + lab.b = 200.0 * (y - z); + + return lab; +} + +static inline CIELAB xrgb32_to_lab(const uint32_t color) +{ + uint8_t r = (color >> 16) & 0xFF; + uint8_t g = (color >> 8) & 0xFF; + uint8_t b = color & 0xFF; + + return rgb888_to_lab(r, g, b); +} + +static inline CIELAB rgb565_to_lab(const uint16_t color) +{ + uint8_t r5 = (color >> 11) & 0x1F; + uint8_t g6 = (color >> 5) & 0x3F; + uint8_t b5 = color & 0x1F; + + /* Convert RGB565 to RGB888 */ + uint8_t r = (r5 * 255) / 31; + uint8_t g = (g6 * 255) / 63; + uint8_t b = (b5 * 255) / 31; + + return rgb888_to_lab(r, g, b); +} + +static inline void sum_lab(CIELAB *sum, const CIELAB lab) +{ + sum->L += lab.L; + sum->a += lab.a; + sum->b += lab.b; +} + +static inline void average_lab(CIELAB *lab, const uint32_t count) +{ + if (count > 0) { + lab->L /= count; + lab->a /= count; + lab->b /= count; + } +} + +static inline double deltaE(const CIELAB lab1, const CIELAB lab2) +{ + return sqrt(pow(lab1.L - lab2.L, 2) + pow(lab1.a - lab2.a, 2) + pow(lab1.b - lab2.b, 2)); +} + +/* + * As color values may vary near the boundary of each bar and also, for computational + * efficiency, check only a small number of pixels (PIXELS_NUM) in the middle of each bar. + */ +static inline bool is_colorbar_ok(const uint8_t *const buf, const struct video_format fmt) +{ + int i; + int bw = fmt.width / BARS_NUM; + CIELAB colorbars[BARS_NUM] = {0}; + + for (int h = 0; h < fmt.height; h++) { + for (i = 0; i < BARS_NUM; i++) { + if (fmt.pixelformat == VIDEO_PIX_FMT_XRGB32) { + uint32_t *pixel = + (uint32_t *)&buf[4 * (h * fmt.width + bw / 2 + i * bw)]; + + for (int j = -PIXELS_NUM / 2; j <= PIXELS_NUM / 2; j++) { + sum_lab(&colorbars[i], xrgb32_to_lab(*(pixel + j))); + } + } else if (fmt.pixelformat == VIDEO_PIX_FMT_RGB565) { + uint16_t *pixel = + (uint16_t *)&buf[2 * (h * fmt.width + bw / 2 + i * bw)]; + + for (int j = -PIXELS_NUM / 2; j <= PIXELS_NUM / 2; j++) { + sum_lab(&colorbars[i], rgb565_to_lab(*(pixel + j))); + } + } else { + printk("Format %d is not supported", fmt.pixelformat); + return false; + } + } + } + + for (i = 0; i < BARS_NUM; i++) { + average_lab(&colorbars[i], PIXELS_NUM * fmt.height); + if (deltaE(colorbars[i], colorbars_target[i]) > LAB_THRESHOLD) { + return false; + } + } + + return true; +} + +#endif /* TEST_PATTERN_CHECK_H_ */ diff --git a/app/common/modules/lvgl_testcard/lvgl_testcard.c b/app/common/modules/lvgl_testcard/lvgl_testcard.c index a7eb248..473d338 100644 --- a/app/common/modules/lvgl_testcard/lvgl_testcard.c +++ b/app/common/modules/lvgl_testcard/lvgl_testcard.c @@ -40,7 +40,7 @@ void lvgl_random_number(void) return; } - LV_IMG_DECLARE(img_testcard_rgb); + LV_IMG_DECLARE(img_testcard_rgb); image = lv_img_create(lv_scr_act()); lv_img_set_src(image, &img_testcard_rgb); lv_obj_align(image, LV_ALIGN_CENTER, 0, 0); diff --git a/app/rt1170/debug.conf b/app/rt1170/debug.conf index 899abee..984eae0 100644 --- a/app/rt1170/debug.conf +++ b/app/rt1170/debug.conf @@ -18,6 +18,7 @@ CONFIG_USB_MASS_STORAGE_LOG_LEVEL_ERR=y CONFIG_USBD_LOG_LEVEL_WRN=y CONFIG_USBH_LOG_LEVEL_WRN=y CONFIG_UDC_DRIVER_LOG_LEVEL_WRN=y +CONFIG_VIDEO_LOG_LEVEL_ERR=y # assert CONFIG_ASSERT=y diff --git a/app/rt1170/prj.conf b/app/rt1170/prj.conf index e60bfba..2e9db6b 100644 --- a/app/rt1170/prj.conf +++ b/app/rt1170/prj.conf @@ -24,6 +24,7 @@ CONFIG_RTC=y CONFIG_SENSOR=y CONFIG_SPI=y CONFIG_USB_DEVICE_STACK_NEXT=y +CONFIG_VIDEO=y CONFIG_WATCHDOG=y # Increase minimum timeout window for mcux driver