diff --git a/MAINTAINERS.yml b/MAINTAINERS.yml index 488e60da433c1..232e5f84a6723 100644 --- a/MAINTAINERS.yml +++ b/MAINTAINERS.yml @@ -3203,6 +3203,21 @@ PHYTEC Platforms: labels: - "platform: PHYTEC" +Pixel library: + status: maintained + maintainers: + - josuah + files: + - include/zephyr/pixel/ + - lib/pixel/ + - tests/lib/pixel/ + - samples/lib/pixel/ + labels: + - "area: Pixel Library" + tests: + - libraries.pixel + description: Library for pixel manipulation and image processing + PMCI: status: maintained maintainers: diff --git a/cmake/linker_script/common/common-ram.cmake b/cmake/linker_script/common/common-ram.cmake index d46376edd03fb..65fcd8cbbba24 100644 --- a/cmake/linker_script/common/common-ram.cmake +++ b/cmake/linker_script/common/common-ram.cmake @@ -138,6 +138,12 @@ if(CONFIG_PCIE) zephyr_iterable_section(NAME pcie_dev GROUP DATA_REGION ${XIP_ALIGN_WITH_INPUT} SUBALIGN ${CONFIG_LINKER_ITERABLE_SUBALIGN}) endif() +if(CONFIG_PIXEL) + zephyr_iterable_section(NAME pixel_convert GROUP DATA_REGION ${XIP_ALIGN_WITH_INPUT} SUBALIGN ${CONFIG_LINKER_ITERABLE_SUBALIGN}) + zephyr_iterable_section(NAME pixel_resize GROUP DATA_REGION ${XIP_ALIGN_WITH_INPUT} SUBALIGN ${CONFIG_LINKER_ITERABLE_SUBALIGN}) + zephyr_iterable_section(NAME pixel_kernel GROUP DATA_REGION ${XIP_ALIGN_WITH_INPUT} SUBALIGN ${CONFIG_LINKER_ITERABLE_SUBALIGN}) +endif() + if(CONFIG_USB_DEVICE_STACK OR CONFIG_USB_DEVICE_STACK_NEXT) zephyr_iterable_section(NAME usbd_context GROUP DATA_REGION ${XIP_ALIGN_WITH_INPUT} SUBALIGN ${CONFIG_LINKER_ITERABLE_SUBALIGN}) zephyr_iterable_section(NAME usbd_class_fs GROUP DATA_REGION ${XIP_ALIGN_WITH_INPUT} SUBALIGN ${CONFIG_LINKER_ITERABLE_SUBALIGN}) diff --git a/include/zephyr/drivers/video.h b/include/zephyr/drivers/video.h index 67212110ebcde..19ca64826e0e4 100644 --- a/include/zephyr/drivers/video.h +++ b/include/zephyr/drivers/video.h @@ -1211,6 +1211,13 @@ void video_closest_frmival(const struct device *dev, enum video_endpoint_id ep, * @{ */ +/** + * @code{.unparsed} + * | RrrGggBb | ... + * @endcode + */ +#define VIDEO_PIX_FMT_RGB332 VIDEO_FOURCC('R', 'G', 'B', '1') + /** * 5 red bits [15:11], 6 green bits [10:5], 5 blue bits [4:0]. * This 16-bit integer is then packed in big endian format over two bytes: @@ -1333,6 +1340,13 @@ void video_closest_frmival(const struct device *dev, enum video_endpoint_id ep, */ #define VIDEO_PIX_FMT_UYVY VIDEO_FOURCC('U', 'Y', 'V', 'Y') +/** + * @code{.unparsed} + * | Yyyyyyyy Uuuuuuuu Vvvvvvvv | ... + * @endcode + */ +#define VIDEO_PIX_FMT_YUV24 VIDEO_FOURCC('Y', 'U', 'V', '3') + /** * The first byte is empty (X) for each pixel. * @@ -1376,6 +1390,7 @@ static inline unsigned int video_bits_per_pixel(uint32_t pixfmt) case VIDEO_PIX_FMT_GRBG8: case VIDEO_PIX_FMT_RGGB8: case VIDEO_PIX_FMT_GREY: + case VIDEO_PIX_FMT_RGB332: return 8; case VIDEO_PIX_FMT_SBGGR10P: case VIDEO_PIX_FMT_SGBRG10P: @@ -1396,6 +1411,7 @@ static inline unsigned int video_bits_per_pixel(uint32_t pixfmt) case VIDEO_PIX_FMT_Y14P: return 14; case VIDEO_PIX_FMT_RGB565: + case VIDEO_PIX_FMT_RGB565X: case VIDEO_PIX_FMT_YUYV: case VIDEO_PIX_FMT_YVYU: case VIDEO_PIX_FMT_UYVY: @@ -1419,6 +1435,7 @@ static inline unsigned int video_bits_per_pixel(uint32_t pixfmt) return 16; case VIDEO_PIX_FMT_BGR24: case VIDEO_PIX_FMT_RGB24: + case VIDEO_PIX_FMT_YUV24: return 24; case VIDEO_PIX_FMT_XRGB32: case VIDEO_PIX_FMT_XYUV32: diff --git a/include/zephyr/pixel/bayer.h b/include/zephyr/pixel/bayer.h new file mode 100644 index 0000000000000..50c88983237b3 --- /dev/null +++ b/include/zephyr/pixel/bayer.h @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_PIXEL_BAYER_H_ +#define ZEPHYR_INCLUDE_PIXEL_BAYER_H_ + +#include +#include + +#include + +/** + * @brief Define a new bayer format conversion operation. + * + * @param _fn Function performing the operation. + * @param _fourcc_in The input format for that operation. + * @param _window_size The number of line of context needed for that debayer operation. + */ +#define PIXEL_DEFINE_BAYER_OPERATION(_fn, _fourcc_in, _window_size) \ + static const STRUCT_SECTION_ITERABLE_ALTERNATE(pixel_convert, pixel_operation, \ + _fn##_op) = { \ + .name = #_fn, \ + .fourcc_in = _fourcc_in, \ + .fourcc_out = VIDEO_PIX_FMT_RGB24, \ + .window_size = _window_size, \ + .run = _fn, \ + } + +/** + * @brief Convert a line from RGGB8 to RGB24 with 3x3 method + * + * @param i0 Buffer of the input row number 0 in bayer format (1 byte per pixel). + * @param i1 Buffer of the input row number 1 in bayer format (1 byte per pixel). + * @param i2 Buffer of the input row number 2 in bayer format (1 byte per pixel). + * @param rgb24 Buffer of the output row in RGB24 format (3 bytes per pixel). + * @param width Width of the lines in number of pixels. + */ +void pixel_line_rggb8_to_rgb24_3x3(const uint8_t *i0, const uint8_t *i1, const uint8_t *i2, + uint8_t *rgb24, uint16_t width); +/** + * @brief Convert a line from GRBG8 to RGB24 with 3x3 method + * @copydetails pixel_line_rggb8_to_rgb24_3x3() + */ +void pixel_line_grbg8_to_rgb24_3x3(const uint8_t *i0, const uint8_t *i1, const uint8_t *i2, + uint8_t *rgb24, uint16_t width); +/** + * @brief Convert a line from BGGR8 to RGB24 with 3x3 method + * @copydetails pixel_line_rggb8_to_rgb24_3x3() + */ +void pixel_line_bggr8_to_rgb24_3x3(const uint8_t *i0, const uint8_t *i1, const uint8_t *i2, + uint8_t *rgb24, uint16_t width); +/** + * @brief Convert a line from GBRG8 to RGB24 with 3x3 method + * @copydetails pixel_line_rggb8_to_rgb24_3x3() + */ +void pixel_line_gbrg8_to_rgb24_3x3(const uint8_t *i0, const uint8_t *i1, const uint8_t *i2, + uint8_t *rgb24, uint16_t width); + +/** + * @brief Convert a line from RGGB8 to RGB24 with 2x2 method + * + * @param i0 Buffer of the input row number 0 in bayer format (1 byte per pixel). + * @param i1 Buffer of the input row number 1 in bayer format (1 byte per pixel). + * @param rgb24 Buffer of the output row in RGB24 format (3 bytes per pixel). + * @param width Width of the lines in number of pixels. + */ +void pixel_line_rggb8_to_rgb24_2x2(const uint8_t *i0, const uint8_t *i1, uint8_t *rgb24, + uint16_t width); +/** + * @brief Convert a line from GBRG8 to RGB24 with 2x2 method + * @copydetails pixel_line_rggb8_to_rgb24_2x2() + */ +void pixel_line_gbrg8_to_rgb24_2x2(const uint8_t *i0, const uint8_t *i1, uint8_t *rgb24, + uint16_t width); +/** + * @brief Convert a line from BGGR8 to RGB24 with 2x2 method + * @copydetails pixel_line_rggb8_to_rgb24_2x2() + */ +void pixel_line_bggr8_to_rgb24_2x2(const uint8_t *i0, const uint8_t *i1, uint8_t *rgb24, + uint16_t width); +/** + * @brief Convert a line from GRBG8 to RGB24 with 2x2 method + * @copydetails pixel_line_rggb8_to_rgb24_2x2() + */ +void pixel_line_grbg8_to_rgb24_2x2(const uint8_t *i0, const uint8_t *i1, uint8_t *rgb24, + uint16_t width); + +#endif /* ZEPHYR_INCLUDE_PIXEL_BAYER_H_ */ diff --git a/include/zephyr/pixel/convert.h b/include/zephyr/pixel/convert.h new file mode 100644 index 0000000000000..3023a9eee3bdf --- /dev/null +++ b/include/zephyr/pixel/convert.h @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_PIXEL_CONVERT_H_ +#define ZEPHYR_INCLUDE_PIXEL_CONVERT_H_ + +#include +#include + +#include +#include +#include + +/** + * @brief Define a new format conversion operation. + * + * @param _fn Function converting one input line. + * @param _fourcc_in The input format for that operation. + * @param _fourcc_out The Output format for that operation. + */ +#define PIXEL_DEFINE_CONVERT_OPERATION(_fn, _fourcc_in, _fourcc_out) \ + static const STRUCT_SECTION_ITERABLE_ALTERNATE(pixel_convert, pixel_operation, \ + _fn##_op) = { \ + .name = #_fn, \ + .fourcc_in = _fourcc_in, \ + .fourcc_out = _fourcc_out, \ + .window_size = 1, \ + .run = pixel_convert_op, \ + .arg = _fn, \ + } +/** + * @brief Helper to turn a format conversion function into an operation. + * + * The line conversion function is free to perform any processing on the input line. + * The @c w argument is the width of both the source and destination buffers. + * + * The line conversion function is to be provided in @c op->arg + * + * @param op Current operation in progress. + */ +void pixel_convert_op(struct pixel_operation *op); + +/** + * @brief Get the luminance (luma channel) of an RGB24 pixel. + * + * @param rgb24 Pointer to an RGB24 pixel: red, green, blue channels. + */ +uint8_t pixel_rgb24_get_luma_bt709(const uint8_t rgb24[3]); + +/** + * @brief Convert a line of pixel data from RGB24 to RGB24 (null conversion). + * + * See @ref video_pixel_formats for the definition of the input and output formats. + * + * You only need to call this function to work directly on raw buffers. + * See @ref pixel_image_convert for converting between formats. + * + * @param src Buffer of the input line in the format, @c XXX in @c pixel_line_XXX_to_YYY(). + * @param dst Buffer of the output line in the format, @c YYY in @c pixel_line_XXX_to_YYY(). + * @param width Width of the lines in number of pixels. + */ +void pixel_line_rgb24_to_rgb24(const uint8_t *src, uint8_t *dst, uint16_t width); +/** + * @brief Convert a line of pixel data from RGB332 to RGB24. + * @copydetails pixel_line_rgb24_to_rgb24() + */ +void pixel_line_rgb332_to_rgb24(const uint8_t *src, uint8_t *dst, uint16_t width); +/** + * @brief Convert a line of pixel data from RGB24 to RGB332 little-endian. + * @copydetails pixel_line_rgb24_to_rgb24() + */ +void pixel_line_rgb24_to_rgb332(const uint8_t *src, uint8_t *dst, uint16_t width); +/** + * @brief Convert a line of pixel data from RGB565 little-endian to RGB24. + * @copydetails pixel_line_rgb24_to_rgb24() + */ +void pixel_line_rgb565le_to_rgb24(const uint8_t *src, uint8_t *dst, uint16_t width); +/** + * @brief Convert a line of pixel data from RGB565 big-endian to RGB24. + * @copydetails pixel_line_rgb24_to_rgb24() + */ +void pixel_line_rgb565be_to_rgb24(const uint8_t *src, uint8_t *dst, uint16_t width); +/** + * @brief Convert a line of pixel data from RGB24 to RGB565 little-endian. + * @copydetails pixel_line_rgb24_to_rgb24() + */ +void pixel_line_rgb24_to_rgb565le(const uint8_t *src, uint8_t *dst, uint16_t width); +/** + * @brief Convert a line of pixel data from RGB24 to RGB565 big-endian. + * @copydetails pixel_line_rgb24_to_rgb24() + */ +void pixel_line_rgb24_to_rgb565be(const uint8_t *src, uint8_t *dst, uint16_t width); +/** + * @brief Convert a line of pixel data from YUYV to RGB24 (BT.709 coefficients). + * @copydetails pixel_line_rgb24_to_rgb24() + */ +void pixel_line_yuyv_to_rgb24_bt709(const uint8_t *src, uint8_t *dst, uint16_t width); +/** + * @brief Convert a line of pixel data from RGB24 to YUYV (BT.709 coefficients). + * @copydetails pixel_line_rgb24_to_rgb24() + */ +void pixel_line_rgb24_to_yuyv_bt709(const uint8_t *src, uint8_t *dst, uint16_t width); +/** + * @brief Convert a line of pixel data from RGB24 to YUV24 (BT.709 coefficients). + * @copydetails pixel_line_rgb24_to_rgb24() + */ +void pixel_line_rgb24_to_yuv24_bt709(const uint8_t *src, uint8_t *dst, uint16_t width); +/** + * @brief Convert a line of pixel data from YUV24 to RGB24 (BT.709 coefficients). + * @copydetails pixel_line_rgb24_to_rgb24() + */ +void pixel_line_yuv24_to_rgb24_bt709(const uint8_t *src, uint8_t *dst, uint16_t width); +/** + * @brief Convert a line of pixel data from YUYV to YUV24 + * @copydetails pixel_line_rgb24_to_rgb24() + */ +void pixel_line_yuyv_to_yuv24(const uint8_t *src, uint8_t *dst, uint16_t width); +/** + * @brief Convert a line of pixel data from YUV24 to YUYV + * @copydetails pixel_line_rgb24_to_rgb24() + */ +void pixel_line_yuv24_to_yuyv(const uint8_t *src, uint8_t *dst, uint16_t width); +/** + * @brief Convert a line of pixel data from Y8 to RGB24 + * @copydetails pixel_line_rgb24_to_rgb24() + */ +void pixel_line_y8_to_rgb24_bt709(const uint8_t *src, uint8_t *dst, uint16_t width); +/** + * @brief Convert a line of pixel data from RGB24 to Y8 + * @copydetails pixel_line_rgb24_to_rgb24() + */ +void pixel_line_rgb24_to_y8_bt709(const uint8_t *src, uint8_t *dst, uint16_t width); + +#endif /* ZEPHYR_INCLUDE_PIXEL_CONVERT_H_ */ diff --git a/include/zephyr/pixel/image.h b/include/zephyr/pixel/image.h new file mode 100644 index 0000000000000..eefa8b301ef16 --- /dev/null +++ b/include/zephyr/pixel/image.h @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_PIXEL_IMAGE_H +#define ZEPHYR_INCLUDE_PIXEL_IMAGE_H + +#include +#include +#include + +/** + * @brief Represent the image currently being processed + */ +struct pixel_image { + sys_slist_t operations; + /** Current width of the image */ + uint16_t width; + /** Current height of the image */ + uint16_t height; + /** Current pixel format of the image as a four character code (fourcc) */ + uint32_t fourcc; + /** Input or output buffer used with the conversion */ + uint8_t *buffer; + /** Size of the input or output buffer */ + size_t size; + /** In case an error occurred, this is set to a matching error code */ + int err; +}; + +/** + * @brief Initialize an image from a memory buffer. + * + * @param img Image to initialize. + * @param buffer Memory containinig input image data to process. + * @param size Total available size in the buffer, can be bigger/smaller than full width x height. + * @param width Width of the complete image in pixels. + * @param height Height of the complete image in pixels. + * @param fourcc Format of data in the buffer as a four-character-code. + */ +void pixel_image_from_buffer(struct pixel_image *img, uint8_t *buffer, size_t size, + uint16_t width, uint16_t height, uint32_t fourcc); + +/** + * @brief Initialize an image from a video buffer. + * + * @param img Image to initialize. + * @param vbuf Video buffer that contains the image data to process. + * @param fmt Format of that video buffer. + */ +void pixel_image_from_vbuf(struct pixel_image *img, struct video_buffer *vbuf, + struct video_format *fmt); + +/** + * @brief Initialize an image from a memory buffer. + * + * @param img Image being processed. + * @param buffer Memory that receives the image data. + * @param size Size of the buffer. + * @return 0 on success + */ +int pixel_image_to_buffer(struct pixel_image *img, uint8_t *buffer, size_t size); + +/** + * @brief Initialize an image from a memory buffer. + * + * @param img Image being processed. + * @param vbuf Video buffer that receives the image data. + * @return 0 on success + */ +int pixel_image_to_vbuf(struct pixel_image *img, struct video_buffer *vbuf); + +/** + * @brief Convert an image to a new pixel format. + * + * An operation is added to convert the image to a new pixel format. + * If the operation to convert the image from the current format to a new format does not exist, + * then the error flag is set, which can be accessed as @c img->err. + * + * In some cases, converting between two formats requires an intermediate conversion to RGB24. + * + * @param img Image to convert. + * @param new_format A four-character-code (FOURCC) as defined by @c . + */ +int pixel_image_convert(struct pixel_image *img, uint32_t new_format); + +/** + * @brief Convert an image from a bayer array format to RGB24. + * + * An operation is added to convert the image to RGB24 using the specified window size, such + * as 2x2 or 3x3. + * + * @note It is also possible to use @ref pixel_image_convert to convert from bayer to RGB24 but + * this does not allow to select the window size. + * + * @param img Image to convert. + * @param window_size The window size for convnerting, usually 2 (faster) or 3 (higher quality). + */ +int pixel_image_debayer(struct pixel_image *img, uint32_t window_size); + +/** + * @brief Resize an image. + * + * An operation is added to change the image size. The aspect ratio is not preserved and the output + * image size is exactly the same as requested. + * + * @param img Image to convert. + * @param width The new width in pixels. + * @param height The new height in pixels. + */ +int pixel_image_resize(struct pixel_image *img, uint16_t width, uint16_t height); + +/** + * @brief Apply a kernel operation on an image. + * + * Kernel operations are working on small blocks of typically 3x3 or 5x5 pixels, repeated over the + * entire image to apply a desired effect on an image. + * + * @param img Image to convert. + * @param kernel_type The type of kernel to apply as defined in + * @param kernel_size The size of the kernel operaiton, usually 3 or 5. + */ +int pixel_image_kernel(struct pixel_image *img, uint32_t kernel_type, int kernel_size); + +/** + * @brief Print an image using higher quality TRUECOLOR terminal escape codes. + * + * @param img Image to print. + */ +void pixel_image_print_truecolor(struct pixel_image *img); + +/** + * @brief Print an image using higher speed 256COLOR terminal escape codes. + * + * @param img Image to print. + */ +void pixel_image_print_256color(struct pixel_image *img); + +/** @cond INTERNAL_HIDDEN */ + +/** + * @brief Add a operation processing step to an image. + * + * @note This is a low-level function only needed to implement new operations. + * + * The operation step will not be processed immediately, but rather added to a linked list of + * operations that will be performed at runtime. + * + * @param img Image to which add a processing step. + * @param template Stream processing step to apply to the image. + * @param buffer_size Size of the input buffer to allocate for this operation. + * @param threshold Minimum number of bytes the operation needs to run one cycle. + * @return 0 on success + */ +int pixel_image_add_operation(struct pixel_image *img, const struct pixel_operation *template, + size_t buffer_size, size_t threshold); + +/** + * @brief Add a operation processing step to an image for uncompressed input data. + * + * @note This is a low-level function only needed to implement new operations. + * + * The operation step will not be processed immediately, but rather added to a linked list of + * operations that will be performed at runtime. + * + * Details such as buffer size or threshold value are deduced from the stream. + * + * @param img Image to which add a processing step. + * @param template Stream processing step to apply to the image. + */ +int pixel_image_add_uncompressed(struct pixel_image *img, const struct pixel_operation *template); + +/** + * @brief Perform all the processing added to the + * + * @note This is a low-level function only needed to implement new operations. + * + * This is where all the image processing happens. The processing steps are not executed while they + * are added to the pipeline, but only while this function is called. + * + * @param img Image to which one or multiple processing steps were added. + * @return 0 on success. + */ +int pixel_image_process(struct pixel_image *img); + +/** + * @brief Perform all the processing added to the + * + * @note This is a low-level function only needed to implement new operations. + * + * This is where all the image processing happens. The processing steps are not executed while they + * are added to the pipeline, but only while this function is called. + * + * @param img Image to which one or multiple processing steps were added. + * @return 0 on success. + */ +int pixel_image_error(struct pixel_image *img, int err); + +/** @endcond */ + +#endif /* ZEPHYR_INCLUDE_PIXEL_IMAGE_H */ diff --git a/include/zephyr/pixel/kernel.h b/include/zephyr/pixel/kernel.h new file mode 100644 index 0000000000000..481ae3803b489 --- /dev/null +++ b/include/zephyr/pixel/kernel.h @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_PIXEL_KERNEL_H_ +#define ZEPHYR_INCLUDE_PIXEL_KERNEL_H_ + +#include +#include + +#include + +/** + * @brief Define a new 5x5 kernel conversion operation. + * + * @param _fn Function converting 5 input lines into 1 output line. + * @param _type Kernel operation type + * @param _fourcc The input format for that operation. + */ +#define PIXEL_DEFINE_KERNEL_5X5_OPERATION(_fn, _type, _fourcc) \ + static const STRUCT_SECTION_ITERABLE_ALTERNATE(pixel_kernel, pixel_operation, \ + _fn##_op) = { \ + .name = #_fn, \ + .fourcc_in = _fourcc, \ + .fourcc_out = _fourcc, \ + .window_size = 5, \ + .run = pixel_kernel_5x5_op, \ + .arg = _fn, \ + .type = _type, \ + } + +/** + * @brief Define a new 3x3 kernel conversion operation. + * + * @param _fn Function converting 3 input lines into 1 output line. + * @param _type Kernel operation type + * @param _fourcc The input format for that operation. + */ +#define PIXEL_DEFINE_KERNEL_3X3_OPERATION(_fn, _type, _fourcc) \ + static const STRUCT_SECTION_ITERABLE_ALTERNATE(pixel_kernel, pixel_operation, \ + _fn##_op) = { \ + .name = #_fn, \ + .fourcc_in = _fourcc, \ + .fourcc_out = _fourcc, \ + .window_size = 3, \ + .run = pixel_kernel_3x3_op, \ + .arg = _fn, \ + .type = _type, \ + } + +/** + * @brief Helper to turn a 5x5 kernel conversion function into an operation. + * + * The line conversion function is free to perform any processing on the input lines and expected + * to produce one output line. + * + * The line conversion function is to be provided in @c op->arg. + * + * @param op Current operation in progress. + */ +void pixel_kernel_5x5_op(struct pixel_operation *op); + +/** + * @brief Helper to turn a 3x3 kernel conversion function into an operation. + * @copydetails pixel_kernel_5x5_op() + */ +void pixel_kernel_3x3_op(struct pixel_operation *op); + +/** + * Available kernel operations to apply to the image. + */ +enum pixel_kernel_type { + /** Identity kernel: no change, the input is the same as the output */ + PIXEL_KERNEL_IDENTITY, + /** Edge detection kernel: keep only an outline of the edges */ + PIXEL_KERNEL_EDGE_DETECT, + /** Gaussian blur kernel: apply a blur onto an image following a Gaussian curve */ + PIXEL_KERNEL_GAUSSIAN_BLUR, + /** Sharpen kernel: accentuate the edges, making the image look less blurry */ + PIXEL_KERNEL_SHARPEN, + /** Denoise kernel: remove the parasitic image noise using the local median value */ + PIXEL_KERNEL_DENOISE, +}; + +/** + * @brief Apply a 3x3 identity kernel to an RGB24 input window and produce one RGB24 line. + * + * @param in Array of input line buffers to convert. + * @param out Pointer to the output line converted. + * @param width Width of the input and output lines in pixels. + */ +void pixel_identity_rgb24_3x3(const uint8_t *in[3], uint8_t *out, uint16_t width); +/** + * @brief Apply a 5x5 identity kernel to an RGB24 input window and produce one RGB24 line. + * @copydetails pixel_identity_rgb24_3x3() + */ +void pixel_identity_rgb24_5x5(const uint8_t *in[5], uint8_t *out, uint16_t width); +/** + * @brief Apply a 3x3 sharpen kernel to an RGB24 input window and produce one RGB24 line. + * @copydetails pixel_identity_rgb24_3x3() + */ +void pixel_sharpen_rgb24_3x3(const uint8_t *in[3], uint8_t *out, uint16_t width); +/** + * @brief Apply a 5x5 unsharp kernel to an RGB24 input window and produce one RGB24 line. + * @copydetails pixel_identity_rgb24_3x3() + */ +void pixel_sharpen_rgb24_5x5(const uint8_t *in[5], uint8_t *out, uint16_t width); +/** + * @brief Apply a 3x3 edge detection kernel to an RGB24 input window and produce one RGB24 line. + * @copydetails pixel_identity_rgb24_3x3() + */ +void pixel_edgedetect_rgb24_3x3(const uint8_t *in[3], uint8_t *out, uint16_t width); +/** + * @brief Apply a 3x3 gaussian blur kernel to an RGB24 input window and produce one RGB24 line. + * @copydetails pixel_identity_rgb24_3x3() + */ +void pixel_gaussianblur_rgb24_3x3(const uint8_t *in[3], uint8_t *out, uint16_t width); +/** + * @brief Apply a 3x3 median denoise kernel to an RGB24 input window and produce one RGB24 line. + * @copydetails pixel_identity_rgb24_3x3() + */ +void pixel_median_rgb24_3x3(const uint8_t *in[3], uint8_t *out, uint16_t width); +/** + * @brief Apply a 5x5 median denoise kernel to an RGB24 input window and produce one RGB24 line. + * @copydetails pixel_identity_rgb24_3x3() + */ +void pixel_median_rgb24_5x5(const uint8_t *in[5], uint8_t *out, uint16_t width); + +#endif /* ZEPHYR_INCLUDE_PIXEL_KERNEL_H_ */ diff --git a/include/zephyr/pixel/operation.h b/include/zephyr/pixel/operation.h new file mode 100644 index 0000000000000..2d9ed11d43156 --- /dev/null +++ b/include/zephyr/pixel/operation.h @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_PIXEL_OPERATION_H +#define ZEPHYR_INCLUDE_PIXEL_OPERATION_H + +#include + +#include +#include +#include +#include +#include +#include + +/** + * @brief One step of a line operation pipeline + * + * @c pixel_operation structs are chained together into a linked list. + * Each step of the linked list contain a ring buffer for the input data, and a pointer to a + * conversion function processing it. + * Along with extra metadata, this is used to process data as a operation of lines. + */ +struct pixel_operation { + sys_snode_t node; + /** Name of the operation, useful for debugging the operation */ + const uint8_t *name; + /** Operation type in case there are several variants for an operation */ + uint32_t type; + /** Pixel input format as a four character code */ + uint32_t fourcc_in; + /** Pixel output format as a four character code */ + uint32_t fourcc_out; + /** Width of the image in pixels */ + uint16_t width; + /** Height of the image in pixels */ + uint16_t height; + /** Current position within the frame */ + uint16_t line_offset; + /** Number of lines of context around the line being converted */ + uint16_t window_size; + /** Number of bytes of input needed to call @c run() */ + uint16_t threshold; + /** Whether the ring buffer memory is allocated on the pixel heap or externally */ + bool is_heap; + /** Ring buffer that keeps track of the position in bytes */ + struct ring_buf ring; + /** Function that performs the I/O */ + void (*run)(struct pixel_operation *op); + /** Operation-specific data */ + void *arg; + /** Timestamp since the op started working in CPU cycles */ + uint32_t start_time; + /** Total time spent working in this op through the operation in CPU cycles */ + uint32_t total_time; +}; + +/** + * @brief Get a pointer to an output line from the next step of the operation. + * + * The buffer obtained can then be used to store the output of the conversion. + * + * The lines will be considered as converted as soon as @ref pixel_operation_done() is called, + * which will feed the line into the next step of the operation. + * + * @param op Current operation in progress. + * @return Pointer to the requested line buffer, never NULL. + */ +static inline uint8_t *pixel_operation_get_output_line(struct pixel_operation *op) +{ + struct pixel_operation *next = SYS_SLIST_PEEK_NEXT_CONTAINER(op, node); + uint32_t pitch; + uint8_t *lines; + uint32_t size; + + __ASSERT_NO_MSG(next != NULL); + + pitch = next->width * video_bits_per_pixel(next->fourcc_in) / BITS_PER_BYTE; + size = ring_buf_put_claim(&next->ring, &lines, pitch); + ring_buf_put_finish(&next->ring, size); + + __ASSERT(size == pitch, + "%s asked for %u output bytes, only have %u out of %u", + op->name, pitch, size, ring_buf_capacity_get(&next->ring)); + + return lines; +} + +/** + * @brief Get a pointer to a given number of input lines, and consume them from the operation. + * + * The lines are considered as processed, which will free them from the input ring buffer, and + * allow more data to flow in. + * + * @param op Current operation in progress. + * @param nb Number of lines to get in one block. + * @return Pointer to the requested number of lines, never NULL. + */ +static inline const uint8_t *pixel_operation_get_input_lines(struct pixel_operation *op, size_t nb) +{ + uint32_t pitch; + uint32_t size; + uint8_t *lines; + + op->line_offset += nb; + + __ASSERT(op->line_offset <= op->height, + "Trying to read at position %u beyond the height of the frame %u", + op->line_offset, op->height); + + pitch = op->width * video_bits_per_pixel(op->fourcc_in) / BITS_PER_BYTE; + size = ring_buf_get_claim(&op->ring, &lines, pitch * nb); + ring_buf_get_finish(&op->ring, size); + + __ASSERT(size == pitch * nb, + "%s asked for %u input bytes, only have %u out of %u", + op->name, pitch, size, ring_buf_capacity_get(&op->ring)); + + return lines; +} + +/** + * @brief Shorthand for @ref pixel_operation_get_input_lines() to get a single input line. + * + * @param op Current operation in progress. + * @return Pointer to the requested number of lines, never NULL. + */ +static inline const uint8_t *pixel_operation_get_input_line(struct pixel_operation *op) +{ + return pixel_operation_get_input_lines(op, 1); +} + +/** + * @brief Request a pointer to the next line of data without affecting the input sream. + * + * This permits to implement a lookahead operation when one or several lines of context is needed + * in addition to the line converted. + * + * @return The pointer to the input data. + */ +static inline uint8_t *pixel_operation_peek_input_line(struct pixel_operation *op) +{ + uint32_t pitch = op->width * video_bits_per_pixel(op->fourcc_in) / BITS_PER_BYTE; + uint8_t *line; + uint32_t size = ring_buf_get_claim(&op->ring, &line, pitch); + + __ASSERT_NO_MSG(size == pitch); + + return line; +} + +/** + * @brief Request a pointer to the entire input buffer content, consumed from the input operation. + * + * @param op Current operation in progress. + * @return The pointer to the input data. + */ +static inline const uint8_t *pixel_operation_get_all_input(struct pixel_operation *op) +{ + uint8_t *remaining; + uint32_t size; + + __ASSERT_NO_MSG(op != NULL); + + op->line_offset = op->height; + + __ASSERT_NO_MSG(op->line_offset <= op->height); + + size = ring_buf_get_claim(&op->ring, &remaining, ring_buf_capacity_get(&op->ring)); + ring_buf_get_finish(&op->ring, size); + + __ASSERT(size == ring_buf_capacity_get(&op->ring), + "Could not dequeue the entire input buffer of %s, %u used, %u free", op->name, + ring_buf_size_get(&op->ring), ring_buf_space_get(&op->ring)); + + return remaining; +} + +static inline void pixel_operation_run(struct pixel_operation *op) +{ + if (op != NULL && op->run != NULL) { + /* Start the counter of the next operation */ + op->start_time = k_cycle_get_32(); + + while (ring_buf_size_get(&op->ring) >= op->threshold && + op->line_offset < op->height) { + op->run(op); + } + } +} + +/** + * @brief Mark the line obtained with @ref pixel_operation_get_output_line as converted. + * + * This will let the next step of the operation know that a new line was converted. + * This allows the pipeline to trigger the next step if there is enough data submitted to it. + * + * @param op Current operation in progress. + */ +static inline void pixel_operation_done(struct pixel_operation *op) +{ + /* Ignore any "peek" operation done previouslyl */ + ring_buf_get_finish(&op->ring, 0); + ring_buf_put_finish(&op->ring, 0); + + /* Flush the timestamp to the counter */ + op->total_time += (op->start_time == 0) ? (0) : (k_cycle_get_32() - op->start_time); + + /* Run the next operation in the chain now that more data is available */ + pixel_operation_run(SYS_SLIST_PEEK_NEXT_CONTAINER(op, node)); + + /* Resuming to this operation, reset the time counter */ + op->start_time = k_cycle_get_32(); +} + +#endif /* ZEPHYR_INCLUDE_PIXEL_OPERATION_H */ diff --git a/include/zephyr/pixel/print.h b/include/zephyr/pixel/print.h new file mode 100644 index 0000000000000..a5f36a5c7b82e --- /dev/null +++ b/include/zephyr/pixel/print.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_PIXEL_PRINT_H_ +#define ZEPHYR_INCLUDE_PIXEL_PRINT_H_ + +#include +#include + +#include + +/** + * @brief Print a buffer using higher quality TRUECOLOR terminal escape codes. + * + * @param buf Imagme buffer to display in the terminal. + * @param size Size of the buffer in bytes. + * @param width Number of pixel of the input buffer in width + * @param height Max number of rows to print + * @param fourcc Format of the buffer to print + */ +void pixel_print_buffer_truecolor(const uint8_t *buf, size_t size, uint16_t width, + uint16_t height, uint32_t fourcc); +/** + * @brief Print a buffer using higher speed 256COLOR terminal escape codes. + * @copydetails pixel_print_buffer_truecolor() + */ +void pixel_print_buffer_256color(const uint8_t *buf, size_t size, uint16_t width, + uint16_t height, uint32_t fourcc); + +/** + * @brief Hexdump a buffer in the RAW8 format + * + * @param buf Input buffer to display in the terminal. + * @param size Size of the input buffer in bytes. + * @param width Number of pixel of the input buffer in width + * @param height Max number of rows to print + */ +void pixel_hexdump_raw8(const uint8_t *buf, size_t size, uint16_t width, uint16_t height); +/** + * @brief Hexdump a buffer in the RGB24 format + * @copydetails pixel_hexdump_raw8() + */ +void pixel_hexdump_rgb24(const uint8_t *buf, size_t size, uint16_t width, uint16_t height); +/** + * @brief Hexdump a buffer in the RGB565 format + * @copydetails pixel_hexdump_raw8() + */ +void pixel_hexdump_rgb565(const uint8_t *buf, size_t size, uint16_t width, uint16_t height); +/** + * @brief Hexdump a buffer in the YUYV format + * @copydetails pixel_hexdump_raw8() + */ +void pixel_hexdump_yuyv(const uint8_t *buf, size_t size, uint16_t width, uint16_t height); + +/** + * @brief Printing RGB histograms to the terminal. + * + * @param rgb24hist Buffer storing 3 histograms one after the other, for the R, G, B channels. + * @param size Total number of buckets in total contained within @p rgb24hist all channels included. + * @param height Desired height of the chart in pixels. + */ +void pixel_print_rgb24hist(const uint16_t *rgb24hist, size_t size, uint16_t height); + +/** + * @brief Printing Y histograms to the terminal. + * + * @param y8hist Buffer storing the histogram for the Y (luma) channel. + * @param size Total number of buckets in total contained within @p hist. + * @param height Desired height of the chart in pixels. + */ +void pixel_print_y8hist(const uint16_t *y8hist, size_t size, uint16_t height); + +/** + * @brief Set the shell instance to use when printing via the shell back-end. + * + * @see CONFIG_PIXEL_PRINT + * + * @param sh Shell instance set as a global variable. + */ +void pixel_print_set_shell(struct shell *sh); + +#endif /* ZEPHYR_INCLUDE_PIXEL_PRINT_H_ */ diff --git a/include/zephyr/pixel/resize.h b/include/zephyr/pixel/resize.h new file mode 100644 index 0000000000000..7a7c827f2fb66 --- /dev/null +++ b/include/zephyr/pixel/resize.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_PIXEL_RESIZE_H_ +#define ZEPHYR_INCLUDE_PIXEL_RESIZE_H_ + +#include + +#include + +/** + * @brief Define a format conversion operation. + * + * Invoking this macro suffices for @ref pixel_image_convert() to include the extra format. + * + * @param _fn Function converting one input line. + * @param _fourcc The pixel format of the data resized. + */ +#define PIXEL_DEFINE_RESIZE_OPERATION(_fn, _fourcc) \ + static const STRUCT_SECTION_ITERABLE_ALTERNATE(pixel_resize, pixel_operation, \ + pixel_resize_op##_fourcc) = { \ + .name = #_fn, \ + .fourcc_in = _fourcc, \ + .fourcc_out = _fourcc, \ + .window_size = 1, \ + .run = _fn, \ + } + +/** + * @brief Resize an 24-bit per pixel frame by subsampling the pixels horizontally/vertically. + * + * @param src_buf Input buffer to resize + * @param src_width Width of the input in number of pixels. + * @param src_height Height of the input in number of pixels. + * @param dst_buf Output buffer in which the stretched/compressed is stored. + * @param dst_width Width of the output in number of pixels. + * @param dst_height Height of the output in number of pixels. + */ +void pixel_resize_frame_raw24(const uint8_t *src_buf, size_t src_width, size_t src_height, + uint8_t *dst_buf, size_t dst_width, size_t dst_height); +/** + * @brief Resize an 16-bit per pixel frame by subsampling the pixels horizontally/vertically. + * @copydetails pixel_resize_frame_raw24() + */ +void pixel_resize_frame_raw16(const uint8_t *src_buf, size_t src_width, size_t src_height, + uint8_t *dst_buf, size_t dst_width, size_t dst_height); +/** + * @brief Resize an 8-bit per pixel frame by subsampling the pixels horizontally/vertically. + * @copydetails pixel_resize_frame_raw24() + */ +void pixel_resize_frame_raw8(const uint8_t *src_buf, size_t src_width, size_t src_height, + uint8_t *dst_buf, size_t dst_width, size_t dst_height); + +#endif /* ZEPHYR_INCLUDE_PIXEL_RESIZE_H_ */ diff --git a/include/zephyr/pixel/stats.h b/include/zephyr/pixel/stats.h new file mode 100644 index 0000000000000..6d6b0e854cfd5 --- /dev/null +++ b/include/zephyr/pixel/stats.h @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_PIXEL_STATS_H +#define ZEPHYR_INCLUDE_PIXEL_STATS_H + +#include + +/** + * @brief Collect red, green, blue channel averages of all pixels in an RGB24 frame. + * + * @param buf Buffer of pixels in RGB24 format (3 bytes per pixel) to collect the statistics from.. + * @param size Size of this input buffer. + * @param rgb24avg The channel averages stored as an RGB24 pixel. + * @param nval The number of values to collect in order to perform the statistics. + */ +void pixel_rgb24frame_to_rgb24avg(const uint8_t *buf, size_t size, uint8_t rgb24avg[3], + uint16_t nval); + +/** + * @brief Collect red, green, blue channel averages of all pixels in an RGGB8 frame. + * + * @param buf Buffer of pixels in bayer format (1 byte per pixel) to collect the statistics from.. + * @param size Size of this input buffer. + * @param width Width of the lines in number of pixels. + * @param rgb24avg The channel averages stored as an RGB24 pixel. + * @param nval The number of values to collect in order to perform the statistics. + */ +void pixel_rggb8frame_to_rgb24avg(const uint8_t *buf, size_t size, uint16_t width, + uint8_t rgb24avg[3], uint16_t nval); +/** + * @brief Collect red, green, blue channel averages of all pixels in an BGGR8 frame. + * @copydetails pixel_rggb8frame_to_rgb24avg() + */ +void pixel_bggr8frame_to_rgb24avg(const uint8_t *buf, size_t size, uint16_t width, + uint8_t rgb24avg[3], uint16_t nval); +/** + * @brief Collect red, green, blue channel averages of all pixels in an GBRG8 frame. + * @copydetails pixel_rggb8frame_to_rgb24avg() + */ +void pixel_gbrg8frame_to_rgb24avg(const uint8_t *buf, size_t size, uint16_t width, + uint8_t rgb24avg[3], uint16_t nval); +/** + * @brief Collect red, green, blue channel averages of all pixels in an GRBG8 frame. + * @copydetails pixel_rggb8frame_to_rgb24avg() + */ +void pixel_grbg8frame_to_rgb24avg(const uint8_t *buf, size_t size, uint16_t width, + uint8_t rgb24avg[3], uint16_t nval); + +/** + * @brief Collect an histogram for each of the red, green, blue channels of an RGB24 frame. + * + * @param buf Buffer of pixels in RGB24 format (3 bytes per pixel) to collect the statistics from.. + * @param buf_size Size of this input buffer. + * @param rgb24hist Buffer storing 3 histograms one after the other, for the R, G, B channels. + * @param hist_size Total number of buckets in the histogram, all channels included. + * @param nval The number of values to collect in order to perform the statistics. + */ +void pixel_rgb24frame_to_rgb24hist(const uint8_t *buf, size_t buf_size, uint16_t *rgb24hist, + size_t hist_size, uint16_t nval); + +/** + * @brief Collect an histogram for each of the red, green, blue channels of an RGGB8 frame. + * + * @param buf Buffer of pixels to collect the statistics from.. + * @param buf_size Size of this input buffer. + * @param width Width of the lines in number of pixels. + * @param rgb24hist Buffer storing 3 histograms one after the other, for the R, G, B channels. + * @param hist_size Total number of buckets in the histogram, all channels included. + * @param nval The number of values to collect in order to perform the statistics. + */ +void pixel_rggb8frame_to_rgb24hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *rgb24hist, size_t hist_size, uint16_t nval); +/** + * @brief Collect an histogram for each of the red, green, blue channels of GBRG8 frame. + * @copydetails pixel_rggb8frame_to_rgb24hist() + */ +void pixel_gbrg8frame_to_rgb24hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *rgb24hist, size_t hist_size, uint16_t nval); +/** + * @brief Collect an histogram for each of the red, green, blue channels of BGGR8 frame. + * @copydetails pixel_rggb8frame_to_rgb24hist() + */ +void pixel_bggr8frame_to_rgb24hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *rgb24hist, size_t hist_size, uint16_t nval); +/** + * @brief Collect an histogram for each of the red, green, blue channels of GRBG8 frame. + * @copydetails pixel_rggb8frame_to_rgb24hist() + */ +void pixel_grbg8frame_to_rgb24hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *rgb24hist, size_t hist_size, uint16_t nval); + +/** + * @brief Collect an histogram for the Y channel, obtained from the pixel values of the image. + * + * @param buf Buffer of pixels in RGB24 format (3 bytes per pixel) to collect the statistics from.. + * @param buf_size Size of this input buffer. + * @param y8hist Buffer storing the histogram for the Y (luma) channel. + * @param hist_size Total number of buckets in the histogram, all channels included. + * @param nval The number of values to collect in order to perform the statistics. + */ +void pixel_rgb24frame_to_y8hist(const uint8_t *buf, size_t buf_size, uint16_t *y8hist, + size_t hist_size, uint16_t nval); + +/** + * @brief Collect an histogram for the Y channel, obtained from the values of an RGGB8 frame. + * + * @param buf Buffer of pixels in bayer format (1 byte per pixel) to collect the statistics from.. + * @param buf_size Size of this input buffer. + * @param width Width of the lines in number of pixels. + * @param y8hist Buffer storing the histogram for the Y (luma) channel. + * @param hist_size Total number of buckets in the histogram, all channels included. + * @param nval The number of values to collect in order to perform the statistics. + */ +void pixel_rggb8frame_to_y8hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *y8hist, size_t hist_size, uint16_t nval); +/** + * @brief Collect an histogram for the Y channel, obtained from the values of an GBRG8 frame. + * @copydetails pixel_rggb8frame_to_y8hist() + */ +void pixel_gbrg8frame_to_y8hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *y8hist, size_t hist_size, uint16_t nval); +/** + * @brief Collect an histogram for the Y channel, obtained from the values of an BGGR8 frame. + * @copydetails pixel_rggb8frame_to_y8hist() + */ +void pixel_bggr8frame_to_y8hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *y8hist, size_t hist_size, uint16_t nval); +/** + * @brief Collect an histogram for the Y channel, obtained from the values of an GRBG8 frame. + * @copydetails pixel_rggb8frame_to_y8hist() + */ +void pixel_grbg8frame_to_y8hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *y8hist, size_t hist_size, uint16_t nval); + +#endif /* ZEPHYR_INCLUDE_PIXEL_STATS_H */ diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index b945968c8fbe7..d8de2732c0f5d 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -16,6 +16,7 @@ add_subdirectory(heap) add_subdirectory(mem_blocks) add_subdirectory_ifdef(CONFIG_NET_BUF net_buf) add_subdirectory(os) +add_subdirectory_ifdef(CONFIG_PIXEL pixel) add_subdirectory(utils) add_subdirectory_ifdef(CONFIG_SMF smf) add_subdirectory_ifdef(CONFIG_OPENAMP open-amp) diff --git a/lib/Kconfig b/lib/Kconfig index ae97399b19953..bf5ae2371ee6d 100644 --- a/lib/Kconfig +++ b/lib/Kconfig @@ -19,6 +19,8 @@ source "lib/net_buf/Kconfig" source "lib/os/Kconfig" +source "lib/pixel/Kconfig" + source "lib/posix/Kconfig" source "lib/open-amp/Kconfig" @@ -32,4 +34,5 @@ source "lib/runtime/Kconfig" source "lib/utils/Kconfig" source "lib/uuid/Kconfig" + endmenu diff --git a/lib/pixel/CMakeLists.txt b/lib/pixel/CMakeLists.txt new file mode 100644 index 0000000000000..3ef8f28fef660 --- /dev/null +++ b/lib/pixel/CMakeLists.txt @@ -0,0 +1,16 @@ +# Copyright The Zephyr Project Contributors +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library() + +# zephyr-keep-sorted-start +zephyr_library_sources(bayer.c) +zephyr_library_sources(convert.c) +zephyr_library_sources(image.c) +zephyr_library_sources(kernel.c) +zephyr_library_sources(print.c) +zephyr_library_sources(resize.c) +zephyr_library_sources(stats.c) +# zephyr-keep-sorted-stop + +zephyr_linker_sources(DATA_SECTIONS pixel.ld) diff --git a/lib/pixel/Kconfig b/lib/pixel/Kconfig new file mode 100644 index 0000000000000..cc811427bf843 --- /dev/null +++ b/lib/pixel/Kconfig @@ -0,0 +1,52 @@ +# Copyright (c) 2025 tinyVision.ai Inc. +# SPDX-License-Identifier: Apache-2.0 + +menuconfig PIXEL + bool "Pixel and Image Manipulation Library" + imply RING_BUFFER_LARGE + +if PIXEL + +config PIXEL_HEAP_SIZE + int "Size in bytes for use in pixel operations intermediate buffers" + default 4096 + help + Every time an operation is performed on an image, intermediate buffers matching the + line width are allocated. This configures how much memory to use for it. + The default is enough for a few conversion for small frames. + +choice PIXEL_PRINT + bool "Select the print function to use for sending characters out." + default PIXEL_PRINT_PRINTF + help + The default is to use printf() as it most often leads to the output being printed out. + +config PIXEL_PRINT_PRINTF + bool "Image output to print is sent to printf()" + help + The data is directly handled by the libc bypassing most configuration. + +config PIXEL_PRINT_PRINTK + bool "Image output to print is sent to printk()" + help + The printk() function will sometimes drop characters to avoid slowing-down the rest of + the firmware, and can be configured to use the logging subsystem. + +config PIXEL_PRINT_SHELL + bool "Image output to print is sent to shell_print()" + help + The shell instance used is set by pixel_print_set_shell(). + +config PIXEL_PRINT_NONE + bool "Image output is dropped and not sent anywhere" + help + This is useful for environments where UTF-8 or escape characters can be a problem, + or where a human is not watching, like CI, or quickly toggle on/off this feature. + +endchoice + +module = PIXEL +module-str = pixel +source "subsys/logging/Kconfig.template.log_config" + +endif diff --git a/lib/pixel/bayer.c b/lib/pixel/bayer.c new file mode 100644 index 0000000000000..6f2f9c88863d2 --- /dev/null +++ b/lib/pixel/bayer.c @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include + +LOG_MODULE_REGISTER(pixel_bayer, CONFIG_PIXEL_LOG_LEVEL); + +int pixel_image_debayer(struct pixel_image *img, uint32_t window_size) +{ + const struct pixel_operation *op = NULL; + + STRUCT_SECTION_FOREACH_ALTERNATE(pixel_convert, pixel_operation, tmp) { + if (tmp->fourcc_in == img->fourcc && tmp->fourcc_out == VIDEO_PIX_FMT_RGB24 && + tmp->window_size == window_size) { + op = tmp; + break; + } + } + + if (op == NULL) { + LOG_ERR("Conversion operation from %s to %s using %ux%u window not found", + VIDEO_FOURCC_TO_STR(img->fourcc), VIDEO_FOURCC_TO_STR(VIDEO_PIX_FMT_RGB24), + window_size, window_size); + return pixel_image_error(img, -ENOSYS); + } + + return pixel_image_add_uncompressed(img, op); +} + +#define FOLD_L_3X3(l0, l1, l2) \ + { \ + {l0[1], l0[0], l0[1]}, \ + {l1[1], l1[0], l1[1]}, \ + {l2[1], l2[0], l2[1]}, \ + } + +#define FOLD_R_3X3(l0, l1, l2, n) \ + { \ + {l0[(n) - 2], l0[(n) - 1], l0[(n) - 2]}, \ + {l1[(n) - 2], l1[(n) - 1], l1[(n) - 2]}, \ + {l2[(n) - 2], l2[(n) - 1], l2[(n) - 2]}, \ + } + +static inline void pixel_rggb8_to_rgb24_3x3(const uint8_t rgr0[3], const uint8_t gbg1[3], + const uint8_t rgr2[3], uint8_t rgb24[3]) +{ + rgb24[0] = ((uint16_t)rgr0[0] + rgr0[2] + rgr2[0] + rgr2[2]) / 4; + rgb24[1] = ((uint16_t)rgr0[1] + gbg1[2] + gbg1[0] + rgr2[1]) / 4; + rgb24[2] = gbg1[1]; +} + +static inline void pixel_bggr8_to_rgb24_3x3(const uint8_t bgb0[3], const uint8_t grg1[3], + const uint8_t bgb2[3], uint8_t rgb24[3]) +{ + rgb24[0] = grg1[1]; + rgb24[1] = ((uint16_t)bgb0[1] + grg1[2] + grg1[0] + bgb2[1]) / 4; + rgb24[2] = ((uint16_t)bgb0[0] + bgb0[2] + bgb2[0] + bgb2[2]) / 4; +} + +static inline void pixel_grbg8_to_rgb24_3x3(const uint8_t grg0[3], const uint8_t bgb1[3], + const uint8_t grg2[3], uint8_t rgb24[3]) +{ + rgb24[0] = ((uint16_t)grg0[1] + grg2[1]) / 2; + rgb24[1] = bgb1[1]; + rgb24[2] = ((uint16_t)bgb1[0] + bgb1[2]) / 2; +} + +static inline void pixel_gbrg8_to_rgb24_3x3(const uint8_t gbg0[3], const uint8_t rgr1[3], + const uint8_t gbg2[3], uint8_t rgb24[3]) +{ + rgb24[0] = ((uint16_t)rgr1[0] + rgr1[2]) / 2; + rgb24[1] = rgr1[1]; + rgb24[2] = ((uint16_t)gbg0[1] + gbg2[1]) / 2; +} + +static inline void pixel_rggb8_to_rgb24_2x2(uint8_t r0, uint8_t g0, uint8_t g1, uint8_t b0, + uint8_t rgb24[3]) +{ + rgb24[0] = r0; + rgb24[1] = ((uint16_t)g0 + g1) / 2; + rgb24[2] = b0; +} + +static inline void pixel_gbrg8_to_rgb24_2x2(uint8_t g1, uint8_t b0, uint8_t r0, uint8_t g0, + uint8_t rgb24[3]) +{ + rgb24[0] = r0; + rgb24[1] = ((uint16_t)g0 + g1) / 2; + rgb24[2] = b0; +} + +static inline void pixel_bggr8_to_rgb24_2x2(uint8_t b0, uint8_t g0, uint8_t g1, uint8_t r0, + uint8_t rgb24[3]) +{ + rgb24[0] = r0; + rgb24[1] = ((uint16_t)g0 + g1) / 2; + rgb24[2] = b0; +} + +static inline void pixel_grbg8_to_rgb24_2x2(uint8_t g1, uint8_t r0, uint8_t b0, uint8_t g0, + uint8_t rgb24[3]) +{ + rgb24[0] = r0; + rgb24[1] = ((uint16_t)g0 + g1) / 2; + rgb24[2] = b0; +} + +__weak void pixel_line_rggb8_to_rgb24_3x3(const uint8_t *i0, const uint8_t *i1, + const uint8_t *i2, uint8_t *o0, uint16_t w) +{ + __ASSERT_NO_MSG(w >= 4 && w % 2 == 0); + uint8_t il[3][3] = FOLD_L_3X3(i0, i1, i2); + uint8_t ir[3][3] = FOLD_R_3X3(i0, i1, i2, w); + + pixel_grbg8_to_rgb24_3x3(il[0], il[1], il[2], &o0[0]); + for (size_t i = 0, o = 3; i + 4 <= w; i += 2, o += 6) { + pixel_rggb8_to_rgb24_3x3(&i0[i + 0], &i1[i + 0], &i2[i + 0], &o0[o + 0]); + pixel_grbg8_to_rgb24_3x3(&i0[i + 1], &i1[i + 1], &i2[i + 1], &o0[o + 3]); + } + pixel_rggb8_to_rgb24_3x3(ir[0], ir[1], ir[2], &o0[w * 3 - 3]); +} + +__weak void pixel_line_grbg8_to_rgb24_3x3(const uint8_t *i0, const uint8_t *i1, + const uint8_t *i2, uint8_t *o0, uint16_t w) +{ + __ASSERT_NO_MSG(w >= 4 && w % 2 == 0); + uint8_t il[3][4] = FOLD_L_3X3(i0, i1, i2); + uint8_t ir[3][4] = FOLD_R_3X3(i0, i1, i2, w); + + pixel_rggb8_to_rgb24_3x3(il[0], il[1], il[2], &o0[0]); + for (size_t i = 0, o = 3; i + 4 <= w; i += 2, o += 6) { + pixel_grbg8_to_rgb24_3x3(&i0[i + 0], &i1[i + 0], &i2[i + 0], &o0[o + 0]); + pixel_rggb8_to_rgb24_3x3(&i0[i + 1], &i1[i + 1], &i2[i + 1], &o0[o + 3]); + } + pixel_grbg8_to_rgb24_3x3(ir[0], ir[1], ir[2], &o0[w * 3 - 3]); +} + +__weak void pixel_line_bggr8_to_rgb24_3x3(const uint8_t *i0, const uint8_t *i1, + const uint8_t *i2, uint8_t *o0, uint16_t w) +{ + __ASSERT_NO_MSG(w >= 4 && w % 2 == 0); + uint8_t il[3][4] = FOLD_L_3X3(i0, i1, i2); + uint8_t ir[3][4] = FOLD_R_3X3(i0, i1, i2, w); + + pixel_gbrg8_to_rgb24_3x3(il[0], il[1], il[2], &o0[0]); + for (size_t i = 0, o = 3; i + 4 <= w; i += 2, o += 6) { + pixel_bggr8_to_rgb24_3x3(&i0[i + 0], &i1[i + 0], &i2[i + 0], &o0[o + 0]); + pixel_gbrg8_to_rgb24_3x3(&i0[i + 1], &i1[i + 1], &i2[i + 1], &o0[o + 3]); + } + pixel_bggr8_to_rgb24_3x3(ir[0], ir[1], ir[2], &o0[w * 3 - 3]); +} + +__weak void pixel_line_gbrg8_to_rgb24_3x3(const uint8_t *i0, const uint8_t *i1, + const uint8_t *i2, uint8_t *o0, uint16_t w) +{ + __ASSERT_NO_MSG(w >= 4 && w % 2 == 0); + uint8_t il[3][4] = FOLD_L_3X3(i0, i1, i2); + uint8_t ir[3][4] = FOLD_R_3X3(i0, i1, i2, w); + + pixel_bggr8_to_rgb24_3x3(il[0], il[1], il[2], &o0[0]); + for (size_t i = 0, o = 3; i + 4 <= w; i += 2, o += 6) { + pixel_gbrg8_to_rgb24_3x3(&i0[i + 0], &i1[i + 0], &i2[i + 0], &o0[o + 0]); + pixel_bggr8_to_rgb24_3x3(&i0[i + 1], &i1[i + 1], &i2[i + 1], &o0[o + 3]); + } + pixel_gbrg8_to_rgb24_3x3(ir[0], ir[1], ir[2], &o0[w * 3 - 3]); +} + +__weak void pixel_line_rggb8_to_rgb24_2x2(const uint8_t *i0, const uint8_t *i1, uint8_t *o0, + uint16_t w) +{ + __ASSERT_NO_MSG(w >= 2 && w % 2 == 0); + + for (size_t i = 0, o = 0; i + 3 <= w; i += 2, o += 6) { + pixel_rggb8_to_rgb24_2x2(i0[i + 0], i0[i + 1], i1[i + 0], i1[i + 1], &o0[o + 0]); + pixel_grbg8_to_rgb24_2x2(i0[i + 1], i0[i + 2], i1[i + 1], i1[i + 2], &o0[o + 3]); + } + pixel_rggb8_to_rgb24_2x2(i0[w - 1], i0[w - 2], i1[w - 1], i1[w - 2], &o0[w * 3 - 6]); + pixel_grbg8_to_rgb24_2x2(i0[w - 2], i0[w - 1], i1[w - 2], i1[w - 1], &o0[w * 3 - 3]); +} + +__weak void pixel_line_gbrg8_to_rgb24_2x2(const uint8_t *i0, const uint8_t *i1, uint8_t *o0, + uint16_t w) +{ + __ASSERT_NO_MSG(w >= 2 && w % 2 == 0); + + for (size_t i = 0, o = 0; i + 3 <= w; i += 2, o += 6) { + pixel_gbrg8_to_rgb24_2x2(i0[i + 0], i0[i + 1], i1[i + 0], i1[i + 1], &o0[o + 0]); + pixel_bggr8_to_rgb24_2x2(i0[i + 1], i0[i + 2], i1[i + 1], i1[i + 2], &o0[o + 3]); + } + pixel_gbrg8_to_rgb24_2x2(i0[w - 1], i0[w - 2], i1[w - 1], i1[w - 2], &o0[w * 3 - 6]); + pixel_bggr8_to_rgb24_2x2(i0[w - 2], i0[w - 1], i1[w - 2], i1[w - 1], &o0[w * 3 - 3]); +} + +__weak void pixel_line_bggr8_to_rgb24_2x2(const uint8_t *i0, const uint8_t *i1, uint8_t *o0, + uint16_t w) +{ + __ASSERT_NO_MSG(w >= 2 && w % 2 == 0); + + for (size_t i = 0, o = 0; i + 3 <= w; i += 2, o += 6) { + pixel_bggr8_to_rgb24_2x2(i0[i + 0], i0[i + 1], i1[i + 0], i1[i + 1], &o0[o + 0]); + pixel_gbrg8_to_rgb24_2x2(i0[i + 1], i0[i + 2], i1[i + 1], i1[i + 2], &o0[o + 3]); + } + pixel_bggr8_to_rgb24_2x2(i0[w - 1], i0[w - 2], i1[w - 1], i1[w - 2], &o0[w * 3 - 6]); + pixel_gbrg8_to_rgb24_2x2(i0[w - 2], i0[w - 1], i1[w - 2], i1[w - 1], &o0[w * 3 - 3]); +} + +__weak void pixel_line_grbg8_to_rgb24_2x2(const uint8_t *i0, const uint8_t *i1, uint8_t *o0, + uint16_t w) +{ + __ASSERT_NO_MSG(w >= 2 && w % 2 == 0); + + for (size_t i = 0, o = 0; i + 3 <= w; i += 2, o += 6) { + pixel_grbg8_to_rgb24_2x2(i0[i + 0], i0[i + 1], i1[i + 0], i1[i + 1], &o0[o + 0]); + pixel_rggb8_to_rgb24_2x2(i0[i + 1], i0[i + 2], i1[i + 1], i1[i + 2], &o0[o + 3]); + } + pixel_grbg8_to_rgb24_2x2(i0[w - 1], i0[w - 2], i1[w - 1], i1[w - 2], &o0[w * 3 - 6]); + pixel_rggb8_to_rgb24_2x2(i0[w - 2], i0[w - 1], i1[w - 2], i1[w - 1], &o0[w * 3 - 3]); +} + +typedef void fn_3x3_t(const uint8_t *i0, const uint8_t *i1, const uint8_t *i2, uint8_t *o0, + uint16_t width); + +typedef void fn_2x2_t(const uint8_t *i0, const uint8_t *i1, uint8_t *o0, uint16_t width); + +static inline void pixel_op_bayer_to_rgb24_3x3(struct pixel_operation *op, fn_3x3_t *fn0, + fn_3x3_t *fn1) +{ + uint16_t prev_line_offset = op->line_offset; + const uint8_t *i0 = pixel_operation_get_input_line(op); + const uint8_t *i1 = pixel_operation_peek_input_line(op); + const uint8_t *i2 = pixel_operation_peek_input_line(op); + + if (prev_line_offset == 0) { + fn1(i1, i0, i1, pixel_operation_get_output_line(op), op->width); + pixel_operation_done(op); + } + + if (prev_line_offset % 2 == 0) { + fn0(i0, i1, i2, pixel_operation_get_output_line(op), op->width); + pixel_operation_done(op); + } else { + fn1(i0, i1, i2, pixel_operation_get_output_line(op), op->width); + pixel_operation_done(op); + } + + if (op->line_offset + 2 == op->height) { + fn0(i1, i2, i1, pixel_operation_get_output_line(op), op->width); + pixel_operation_done(op); + + /* Skip the two lines of lookahead context, now that the conversion is complete */ + pixel_operation_get_input_line(op); + pixel_operation_get_input_line(op); + } +} + +static inline void pixel_op_bayer_to_rgb24_2x2(struct pixel_operation *op, fn_2x2_t *fn0, + fn_2x2_t *fn1) +{ + uint16_t prev_line_offset = op->line_offset; + const uint8_t *i0 = pixel_operation_get_input_line(op); + const uint8_t *i1 = pixel_operation_peek_input_line(op); + + if (prev_line_offset % 2 == 0) { + fn0(i0, i1, pixel_operation_get_output_line(op), op->width); + pixel_operation_done(op); + } else { + fn1(i0, i1, pixel_operation_get_output_line(op), op->width); + pixel_operation_done(op); + } + + if (op->line_offset + 1 == op->height) { + fn0(i0, i1, pixel_operation_get_output_line(op), op->width); + pixel_operation_done(op); + + /* Skip the two lines of lookahead context, now that the conversion is complete */ + pixel_operation_get_input_line(op); + } +} + +static void pixel_op_rggb8_to_rgb24_3x3(struct pixel_operation *op) +{ + pixel_op_bayer_to_rgb24_3x3(op, &pixel_line_rggb8_to_rgb24_3x3, + &pixel_line_gbrg8_to_rgb24_3x3); +} +PIXEL_DEFINE_BAYER_OPERATION(pixel_op_rggb8_to_rgb24_3x3, VIDEO_PIX_FMT_RGGB8, 3); + +static void pixel_op_gbrg8_to_rgb24_3x3(struct pixel_operation *op) +{ + pixel_op_bayer_to_rgb24_3x3(op, &pixel_line_gbrg8_to_rgb24_3x3, + &pixel_line_rggb8_to_rgb24_3x3); +} +PIXEL_DEFINE_BAYER_OPERATION(pixel_op_gbrg8_to_rgb24_3x3, VIDEO_PIX_FMT_GBRG8, 3); + +static void pixel_op_bggr8_to_rgb24_3x3(struct pixel_operation *op) +{ + pixel_op_bayer_to_rgb24_3x3(op, &pixel_line_bggr8_to_rgb24_3x3, + &pixel_line_grbg8_to_rgb24_3x3); +} +PIXEL_DEFINE_BAYER_OPERATION(pixel_op_bggr8_to_rgb24_3x3, VIDEO_PIX_FMT_BGGR8, 3); + +static void pixel_op_grbg8_to_rgb24_3x3(struct pixel_operation *op) +{ + pixel_op_bayer_to_rgb24_3x3(op, &pixel_line_grbg8_to_rgb24_3x3, + &pixel_line_bggr8_to_rgb24_3x3); +} +PIXEL_DEFINE_BAYER_OPERATION(pixel_op_grbg8_to_rgb24_3x3, VIDEO_PIX_FMT_GRBG8, 3); + +static void pixel_op_rggb8_to_rgb24_2x2(struct pixel_operation *op) +{ + pixel_op_bayer_to_rgb24_2x2(op, &pixel_line_rggb8_to_rgb24_2x2, + &pixel_line_gbrg8_to_rgb24_2x2); +} +PIXEL_DEFINE_BAYER_OPERATION(pixel_op_rggb8_to_rgb24_2x2, VIDEO_PIX_FMT_RGGB8, 2); + +static void pixel_op_gbrg8_to_rgb24_2x2(struct pixel_operation *op) +{ + pixel_op_bayer_to_rgb24_2x2(op, &pixel_line_gbrg8_to_rgb24_2x2, + &pixel_line_rggb8_to_rgb24_2x2); +} +PIXEL_DEFINE_BAYER_OPERATION(pixel_op_gbrg8_to_rgb24_2x2, VIDEO_PIX_FMT_GBRG8, 2); + +static void pixel_op_bggr8_to_rgb24_2x2(struct pixel_operation *op) +{ + pixel_op_bayer_to_rgb24_2x2(op, &pixel_line_bggr8_to_rgb24_2x2, + &pixel_line_grbg8_to_rgb24_2x2); +} +PIXEL_DEFINE_BAYER_OPERATION(pixel_op_bggr8_to_rgb24_2x2, VIDEO_PIX_FMT_BGGR8, 2); + +static void pixel_op_grbg8_to_rgb24_2x2(struct pixel_operation *op) +{ + pixel_op_bayer_to_rgb24_2x2(op, &pixel_line_grbg8_to_rgb24_2x2, + pixel_line_bggr8_to_rgb24_2x2); +} +PIXEL_DEFINE_BAYER_OPERATION(pixel_op_grbg8_to_rgb24_2x2, VIDEO_PIX_FMT_GRBG8, 2); diff --git a/lib/pixel/convert.c b/lib/pixel/convert.c new file mode 100644 index 0000000000000..381ead7481464 --- /dev/null +++ b/lib/pixel/convert.c @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +LOG_MODULE_REGISTER(pixel_convert, CONFIG_PIXEL_LOG_LEVEL); + +int pixel_image_convert(struct pixel_image *img, uint32_t new_format) +{ + const struct pixel_operation *op = NULL; + + STRUCT_SECTION_FOREACH_ALTERNATE(pixel_convert, pixel_operation, tmp) { + if (tmp->fourcc_in == img->fourcc && tmp->fourcc_out == new_format) { + op = tmp; + break; + } + } + + if (op == NULL) { + LOG_ERR("Conversion operation from %s to %s not found", + VIDEO_FOURCC_TO_STR(img->fourcc), VIDEO_FOURCC_TO_STR(new_format)); + return pixel_image_error(img, -ENOSYS); + } + + return pixel_image_add_uncompressed(img, op); +} + +void pixel_convert_op(struct pixel_operation *op) +{ + const uint8_t *line_in = pixel_operation_get_input_line(op); + uint8_t *line_out = pixel_operation_get_output_line(op); + void (*convert)(const uint8_t *rgb24i, uint8_t *rgb24o, uint16_t width) = op->arg; + + __ASSERT_NO_MSG(convert != NULL); + + convert(line_in, line_out, op->width); + pixel_operation_done(op); +} + +__weak void pixel_line_rgb24_to_rgb24(const uint8_t *rgb24i, uint8_t *rgb24o, uint16_t width) +{ + memcpy(rgb24o, rgb24i, width * 3); +} +PIXEL_DEFINE_CONVERT_OPERATION(pixel_line_rgb24_to_rgb24, + VIDEO_PIX_FMT_RGB24, VIDEO_PIX_FMT_RGB24); + +__weak void pixel_line_rgb24_to_rgb332(const uint8_t *rgb24, uint8_t *rgb332, uint16_t width) +{ + for (size_t i = 0, o = 0, w = 0; w < width; w++, i += 3, o += 1) { + rgb332[o] = 0; + rgb332[o] |= (uint16_t)rgb24[i + 0] >> 5 << (0 + 3 + 2); + rgb332[o] |= (uint16_t)rgb24[i + 1] >> 5 << (0 + 0 + 2); + rgb332[o] |= (uint16_t)rgb24[i + 2] >> 6 << (0 + 0 + 0); + } +} +PIXEL_DEFINE_CONVERT_OPERATION(pixel_line_rgb24_to_rgb332, + VIDEO_PIX_FMT_RGB24, VIDEO_PIX_FMT_RGB332); + +__weak void pixel_line_rgb332_to_rgb24(const uint8_t *rgb332, uint8_t *rgb24, uint16_t width) +{ + for (size_t i = 0, o = 0, w = 0; w < width; w++, i += 1, o += 3) { + rgb24[o + 0] = rgb332[i] >> (0 + 3 + 2) << 5; + rgb24[o + 1] = rgb332[i] >> (0 + 0 + 2) << 5; + rgb24[o + 2] = rgb332[i] >> (0 + 0 + 0) << 6; + } +} +PIXEL_DEFINE_CONVERT_OPERATION(pixel_line_rgb332_to_rgb24, + VIDEO_PIX_FMT_RGB332, VIDEO_PIX_FMT_RGB24); + +static inline uint16_t pixel_rgb24_to_rgb565(const uint8_t rgb24[3]) +{ + uint16_t rgb565 = 0; + + rgb565 |= ((uint16_t)rgb24[0] >> 3 << (0 + 6 + 5)); + rgb565 |= ((uint16_t)rgb24[1] >> 2 << (0 + 0 + 5)); + rgb565 |= ((uint16_t)rgb24[2] >> 3 << (0 + 0 + 0)); + return rgb565; +} + +static inline void pixel_rgb565_to_rgb24(uint16_t rgb565, uint8_t rgb24[3]) +{ + rgb24[0] = rgb565 >> (0 + 6 + 5) << 3; + rgb24[1] = rgb565 >> (0 + 0 + 5) << 2; + rgb24[2] = rgb565 >> (0 + 0 + 0) << 3; +} + +__weak void pixel_line_rgb24_to_rgb565be(const uint8_t *rgb24, uint8_t *rgb565be, uint16_t width) +{ + for (size_t i = 0, o = 0, w = 0; w < width; w++, i += 3, o += 2) { + *(uint16_t *)&rgb565be[o] = sys_cpu_to_be16(pixel_rgb24_to_rgb565(&rgb24[i])); + } +} +PIXEL_DEFINE_CONVERT_OPERATION(pixel_line_rgb24_to_rgb565be, + VIDEO_PIX_FMT_RGB24, VIDEO_PIX_FMT_RGB565X); + +__weak void pixel_line_rgb24_to_rgb565le(const uint8_t *rgb24, uint8_t *rgb565le, uint16_t width) +{ + for (size_t i = 0, o = 0, w = 0; w < width; w++, i += 3, o += 2) { + *(uint16_t *)&rgb565le[o] = sys_cpu_to_le16(pixel_rgb24_to_rgb565(&rgb24[i])); + } +} +PIXEL_DEFINE_CONVERT_OPERATION(pixel_line_rgb24_to_rgb565le, + VIDEO_PIX_FMT_RGB24, VIDEO_PIX_FMT_RGB565); + +__weak void pixel_line_rgb565be_to_rgb24(const uint8_t *rgb565be, uint8_t *rgb24, uint16_t width) +{ + for (size_t i = 0, o = 0, w = 0; w < width; w++, i += 2, o += 3) { + pixel_rgb565_to_rgb24(sys_be16_to_cpu(*(uint16_t *)&rgb565be[i]), &rgb24[o]); + } +} +PIXEL_DEFINE_CONVERT_OPERATION(pixel_line_rgb565be_to_rgb24, + VIDEO_PIX_FMT_RGB565X, VIDEO_PIX_FMT_RGB24); + +__weak void pixel_line_rgb565le_to_rgb24(const uint8_t *rgb565le, uint8_t *rgb24, uint16_t width) +{ + for (size_t i = 0, o = 0, w = 0; w < width; w++, i += 2, o += 3) { + pixel_rgb565_to_rgb24(sys_le16_to_cpu(*(uint16_t *)&rgb565le[i]), &rgb24[o]); + } +} +PIXEL_DEFINE_CONVERT_OPERATION(pixel_line_rgb565le_to_rgb24, + VIDEO_PIX_FMT_RGB565, VIDEO_PIX_FMT_RGB24); + +#define Q21(val) ((int32_t)((val) * (1 << 21))) + +static inline uint8_t pixel_rgb24_to_y8_bt709(const uint8_t rgb24[3]) +{ + int16_t r = rgb24[0], g = rgb24[1], b = rgb24[2]; + + return CLAMP(((Q21(+0.1826) * r + Q21(+0.6142) * g + Q21(+0.0620) * b) >> 21) + 16, + 0x00, 0xff); +} + +uint8_t pixel_rgb24_get_luma_bt709(const uint8_t rgb24[3]) +{ + return pixel_rgb24_to_y8_bt709(rgb24); +} + +static inline uint8_t pixel_rgb24_to_u8_bt709(const uint8_t rgb24[3]) +{ + int16_t r = rgb24[0], g = rgb24[1], b = rgb24[2]; + + return CLAMP(((Q21(-0.1006) * r + Q21(-0.3386) * g + Q21(+0.4392) * b) >> 21) + 128, + 0x00, 0xff); +} + +static inline uint8_t pixel_rgb24_to_v8_bt709(const uint8_t rgb24[3]) +{ + int16_t r = rgb24[0], g = rgb24[1], b = rgb24[2]; + + return CLAMP(((Q21(+0.4392) * r + Q21(-0.3989) * g + Q21(-0.0403) * b) >> 21) + 128, + 0x00, 0xff); +} + +static inline void pixel_yuv24_to_rgb24_bt709(const uint8_t y, uint8_t u, uint8_t v, + uint8_t rgb24[3]) +{ + int32_t yy = (int32_t)y - 16, uu = (int32_t)u - 128, vv = (int32_t)v - 128; + + /* Y range [16:235], U/V range [16:240], RGB range[0:255] (full range) */ + rgb24[0] = CLAMP((Q21(+1.1644) * yy + Q21(+0.0000) * uu + Q21(+1.7928) * vv) >> 21, + 0x00, 0xff); + rgb24[1] = CLAMP((Q21(+1.1644) * yy + Q21(-0.2133) * uu + Q21(-0.5330) * vv) >> 21, + 0x00, 0xff); + rgb24[2] = CLAMP((Q21(+1.1644) * yy + Q21(+2.1124) * uu + Q21(+0.0000) * vv) >> 21, + 0x00, 0xff); +} + +#undef Q21 + +__weak void pixel_line_yuv24_to_rgb24_bt709(const uint8_t *yuv24, uint8_t *rgb24, uint16_t width) +{ + for (size_t i = 0, o = 0, w = 0; w < width; w++, i += 3, o += 3) { + pixel_yuv24_to_rgb24_bt709(yuv24[i + 0], yuv24[i + 1], yuv24[i + 2], &rgb24[o]); + } +} +PIXEL_DEFINE_CONVERT_OPERATION(pixel_line_yuv24_to_rgb24_bt709, + VIDEO_PIX_FMT_YUV24, VIDEO_PIX_FMT_RGB24); + +void pixel_line_rgb24_to_yuv24_bt709(const uint8_t *rgb24, uint8_t *yuv24, uint16_t width) +{ + for (size_t i = 0, o = 0, w = 0; w < width; w++, i += 3, o += 3) { + yuv24[o + 0] = pixel_rgb24_to_y8_bt709(&rgb24[i]); + yuv24[o + 1] = pixel_rgb24_to_u8_bt709(&rgb24[i]); + yuv24[o + 2] = pixel_rgb24_to_v8_bt709(&rgb24[i]); + } +} +PIXEL_DEFINE_CONVERT_OPERATION(pixel_line_rgb24_to_yuv24_bt709, + VIDEO_PIX_FMT_RGB24, VIDEO_PIX_FMT_YUV24); + +__weak void pixel_line_yuv24_to_yuyv(const uint8_t *yuv24, uint8_t *yuyv, uint16_t width) +{ + for (size_t i = 0, o = 0, w = 0; w + 2 <= width; w += 2, i += 6, o += 4) { + /* Pixel 0 */ + yuyv[o + 0] = yuv24[i + 0]; + yuyv[o + 1] = yuv24[i + 1]; + /* Pixel 1 */ + yuyv[o + 2] = yuv24[i + 3]; + yuyv[o + 3] = yuv24[i + 5]; + } +} +PIXEL_DEFINE_CONVERT_OPERATION(pixel_line_yuv24_to_yuyv, + VIDEO_PIX_FMT_YUV24, VIDEO_PIX_FMT_YUYV); + +__weak void pixel_line_yuyv_to_yuv24(const uint8_t *yuyv, uint8_t *yuv24, uint16_t width) +{ + for (size_t i = 0, o = 0, w = 0; w + 2 <= width; w += 2, i += 4, o += 6) { + /* Pixel 0 */ + yuv24[o + 0] = yuyv[i + 0]; + yuv24[o + 1] = yuyv[i + 1]; + yuv24[o + 2] = yuyv[i + 3]; + /* Pixel 1 */ + yuv24[o + 3] = yuyv[i + 2]; + yuv24[o + 4] = yuyv[i + 1]; + yuv24[o + 5] = yuyv[i + 3]; + } +} +PIXEL_DEFINE_CONVERT_OPERATION(pixel_line_yuyv_to_yuv24, + VIDEO_PIX_FMT_YUYV, VIDEO_PIX_FMT_YUV24); + +__weak void pixel_line_rgb24_to_yuyv_bt709(const uint8_t *rgb24, uint8_t *yuyv, uint16_t width) +{ + for (size_t i = 0, o = 0, w = 0; w + 2 <= width; w += 2, i += 6, o += 4) { + /* Pixel 0 */ + yuyv[o + 0] = pixel_rgb24_to_y8_bt709(&rgb24[i + 0]); + yuyv[o + 1] = pixel_rgb24_to_u8_bt709(&rgb24[i + 0]); + /* Pixel 1 */ + yuyv[o + 2] = pixel_rgb24_to_y8_bt709(&rgb24[i + 3]); + yuyv[o + 3] = pixel_rgb24_to_v8_bt709(&rgb24[i + 3]); + } +} +PIXEL_DEFINE_CONVERT_OPERATION(pixel_line_rgb24_to_yuyv_bt709, + VIDEO_PIX_FMT_RGB24, VIDEO_PIX_FMT_YUYV); + +__weak void pixel_line_yuyv_to_rgb24_bt709(const uint8_t *yuyv, uint8_t *rgb24, uint16_t width) +{ + for (size_t i = 0, o = 0, w = 0; w + 2 <= width; w += 2, i += 4, o += 6) { + /* Pixel 0 */ + pixel_yuv24_to_rgb24_bt709(yuyv[i + 0], yuyv[i + 1], yuyv[i + 3], &rgb24[o + 0]); + /* Pixel 1 */ + pixel_yuv24_to_rgb24_bt709(yuyv[i + 2], yuyv[i + 1], yuyv[i + 3], &rgb24[o + 3]); + } +} +PIXEL_DEFINE_CONVERT_OPERATION(pixel_line_yuyv_to_rgb24_bt709, + VIDEO_PIX_FMT_YUYV, VIDEO_PIX_FMT_RGB24); + +__weak void pixel_line_y8_to_rgb24_bt709(const uint8_t *y8, uint8_t *rgb24, uint16_t width) +{ + for (size_t i = 0, o = 0, w = 0; w < width; w++, i += 1, o += 3) { + pixel_yuv24_to_rgb24_bt709(y8[i], UINT8_MAX / 2, UINT8_MAX / 2, &rgb24[o]); + } +} +PIXEL_DEFINE_CONVERT_OPERATION(pixel_line_y8_to_rgb24_bt709, + VIDEO_PIX_FMT_GREY, VIDEO_PIX_FMT_RGB24); + +__weak void pixel_line_rgb24_to_y8_bt709(const uint8_t *rgb24, uint8_t *y8, uint16_t width) +{ + for (size_t i = 0, o = 0, w = 0; w < width; w++, i += 3, o += 1) { + y8[o] = pixel_rgb24_to_y8_bt709(&rgb24[i]); + } +} +PIXEL_DEFINE_CONVERT_OPERATION(pixel_line_rgb24_to_y8_bt709, + VIDEO_PIX_FMT_RGB24, VIDEO_PIX_FMT_GREY); diff --git a/lib/pixel/image.c b/lib/pixel/image.c new file mode 100644 index 0000000000000..dcc2d21fab7e5 --- /dev/null +++ b/lib/pixel/image.c @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include + +LOG_MODULE_REGISTER(pixel_image, CONFIG_PIXEL_LOG_LEVEL); + +K_HEAP_DEFINE(pixel_heap, CONFIG_PIXEL_HEAP_SIZE); + +static void pixel_image_free(struct pixel_image *img) +{ + struct pixel_operation *op, *tmp; + + SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&img->operations, op, tmp, node) { + if (op->is_heap) { + k_heap_free(&pixel_heap, op->ring.buffer); + } + memset(op, 0x00, sizeof(*op)); + k_heap_free(&pixel_heap, op); + } + memset(&img->operations, 0x00, sizeof(img->operations)); +} + +int pixel_image_error(struct pixel_image *img, int err) +{ + if (err != 0 && img->err == 0) { + pixel_image_free(img); + img->err = err; + } + return err; +} + +static void *pixel_image_alloc(struct pixel_image *img, size_t size) +{ + void *mem; + + mem = k_heap_alloc(&pixel_heap, size, K_NO_WAIT); + if (mem == NULL) { + LOG_ERR("Out of memory whle allocating %zu bytes", size); + } + return mem; +} + +int pixel_image_add_operation(struct pixel_image *img, const struct pixel_operation *template, + size_t buffer_size, size_t threshold) +{ + struct pixel_operation *op; + + if (img->err) { + return -ECANCELED; + } + + if (template->fourcc_in != img->fourcc) { + LOG_ERR("Wrong format for this operation: image has %s, operation uses %s", + VIDEO_FOURCC_TO_STR(template->fourcc_in), VIDEO_FOURCC_TO_STR(img->fourcc)); + return pixel_image_error(img, -EINVAL); + } + + op = pixel_image_alloc(img, sizeof(*op)); + if (op == NULL) { + return pixel_image_error(img, -ENOMEM); + } + + memcpy(op, template, sizeof(*op)); + op->threshold = threshold; + op->width = img->width; + op->height = img->height; + op->ring.buffer = NULL; /* allocated later */ + op->ring.size = buffer_size; + + img->fourcc = op->fourcc_out; + + sys_slist_append(&img->operations, &op->node); + + return 0; +} + +int pixel_image_add_uncompressed(struct pixel_image *img, const struct pixel_operation *template) +{ + size_t bpp = video_bits_per_pixel(img->fourcc); + size_t size = template->window_size * img->width * bpp / BITS_PER_BYTE; + + __ASSERT(bpp > 0, "Unknown image pitch for format %s.", VIDEO_FOURCC_TO_STR(img->fourcc)); + + return pixel_image_add_operation(img, template, size, size); +} + +int pixel_image_process(struct pixel_image *img) +{ + struct pixel_operation *op; + uint8_t *p; + + if (img->err) { + return -ECANCELED; + } + + if (img->buffer == NULL) { + LOG_ERR("No input buffer configured"); + return pixel_image_error(img, -ENOBUFS); + } + + op = SYS_SLIST_PEEK_HEAD_CONTAINER(&img->operations, op, node); + if (op == NULL) { + LOG_ERR("No operation to perform on image"); + return pixel_image_error(img, -ENOSYS); + } + + if (ring_buf_capacity_get(&op->ring) < op->ring.size) { + LOG_ERR("Not enough space (%u) in input buffer to run the first operation (%u)", + ring_buf_capacity_get(&op->ring), op->ring.size); + return pixel_image_error(img, -ENOSPC); + } + + ring_buf_init(&op->ring, img->size, img->buffer); + ring_buf_put_claim(&op->ring, &p, img->size); + ring_buf_put_finish(&op->ring, img->size); + + while ((op = SYS_SLIST_PEEK_NEXT_CONTAINER(op, node)) != NULL) { + if (op->ring.buffer == NULL) { + op->ring.buffer = pixel_image_alloc(img, op->ring.size); + if (op->ring.buffer == NULL) { + return pixel_image_error(img, -ENOMEM); + } + op->is_heap = true; + } + } + + SYS_SLIST_FOR_EACH_CONTAINER(&img->operations, op, node) { + LOG_DBG("- %s %ux%u to %s, %s, threshold %u", + VIDEO_FOURCC_TO_STR(op->fourcc_in), op->width, op->height, + VIDEO_FOURCC_TO_STR(op->fourcc_out), op->name, op->threshold); + } + + op = SYS_SLIST_PEEK_HEAD_CONTAINER(&img->operations, op, node); + + pixel_operation_run(op); + pixel_image_free(img); + + return 0; +} + +void pixel_image_from_buffer(struct pixel_image *img, uint8_t *buffer, size_t size, + uint16_t width, uint16_t height, uint32_t fourcc) +{ + memset(img, 0x00, sizeof(*img)); + img->buffer = buffer; + img->size = size; + img->width = width; + img->height = height; + img->fourcc = fourcc; +} + +void pixel_image_from_vbuf(struct pixel_image *img, struct video_buffer *vbuf, + struct video_format *fmt) +{ + pixel_image_from_buffer(img, vbuf->buffer, vbuf->size, fmt->width, fmt->height, + fmt->pixelformat); +} + +int pixel_image_to_buffer(struct pixel_image *img, uint8_t *buffer, size_t size) +{ + struct pixel_operation *op; + int ret; + + if (img->err) { + return -ECANCELED; + } + + op = pixel_image_alloc(img, sizeof(struct pixel_operation)); + if (op == NULL) { + return pixel_image_error(img, -ENOMEM); + } + + memset(op, 0x00, sizeof(*op)); + op->name = __func__; + op->threshold = size; + op->fourcc_in = img->fourcc; + op->fourcc_out = img->fourcc; + op->width = img->width; + op->height = img->height; + op->ring.buffer = buffer; + op->ring.size = size; + + sys_slist_append(&img->operations, &op->node); + + ret = pixel_image_process(img); + + img->buffer = buffer; + img->size = size; + + return ret; +} + +int pixel_image_to_vbuf(struct pixel_image *img, struct video_buffer *vbuf) +{ + int ret; + + ret = pixel_image_to_buffer(img, vbuf->buffer, vbuf->size); + vbuf->bytesused = img->size; + + return ret; +} diff --git a/lib/pixel/kernel.c b/lib/pixel/kernel.c new file mode 100644 index 0000000000000..a7d158d70cbb4 --- /dev/null +++ b/lib/pixel/kernel.c @@ -0,0 +1,433 @@ +/* + * Copyir (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include + +LOG_MODULE_REGISTER(pixel_kernel, CONFIG_PIXEL_LOG_LEVEL); + +int pixel_image_kernel(struct pixel_image *img, uint32_t kernel_type, int kernel_size) +{ + const struct pixel_operation *op = NULL; + + STRUCT_SECTION_FOREACH_ALTERNATE(pixel_kernel, pixel_operation, tmp) { + if (tmp->fourcc_in == img->fourcc && tmp->type == kernel_type && + kernel_size == tmp->window_size) { + op = tmp; + break; + } + } + + if (op == NULL) { + LOG_ERR("Kernel operation %u of size %ux%u on %s data not found", + kernel_type, kernel_size, kernel_size, VIDEO_FOURCC_TO_STR(img->fourcc)); + return pixel_image_error(img, -ENOSYS); + } + + return pixel_image_add_uncompressed(img, op); +} + +/* Function that processes a 3x3 or 5x5 pixel block described by line buffers and column indexes */ +typedef void kernel_3x3_t(const uint8_t *in[3], int i0, int i1, int i2, + uint8_t *out, int o0, uint16_t base, const uint16_t *kernel); +typedef void kernel_5x5_t(const uint8_t *in[3], int i0, int i1, int i2, int i3, int i4, + uint8_t *out, int o0, uint16_t base, const uint16_t *kernel); + +/* Function that repeats a 3x3 or 5x5 block operation to each channel of a pixel format */ +typedef void pixfmt_3x3_t(const uint8_t *in[3], int i0, int i1, int i2, + uint8_t *out, int o0, uint16_t base, kernel_3x3_t *line_fn, + const uint16_t *kernel); +typedef void pixfmt_5x5_t(const uint8_t *in[5], int i0, int i1, int i2, int i3, int i4, + uint8_t *out, int o0, uint16_t base, kernel_5x5_t *line_fn, + const uint16_t *kernel); + +/* Function that repeats a 3x3 or 5x5 kernel operation over an entire line line */ +typedef void line_3x3_t(const uint8_t *in[3], uint8_t *out, uint16_t width); +typedef void line_5x5_t(const uint8_t *in[5], uint8_t *out, uint16_t width); + +/* + * Convolution kernels: multiply a grid of coefficient with the input data and um them to produce + * one output value. + */ + +static void pixel_convolve_3x3(const uint8_t *in[3], int i0, int i1, int i2, + uint8_t *out, int o0, uint16_t base, const uint16_t *kernel) +{ + int16_t result = 0; + int k = 0; + + /* Apply the coefficients on 3 rows */ + for (int h = 0; h < 3; h++) { + /* Apply the coefficients on 5 columns */ + result += in[h][base + i0] * kernel[k++]; /* line h column 0 */ + result += in[h][base + i1] * kernel[k++]; /* line h column 1 */ + result += in[h][base + i2] * kernel[k++]; /* line h column 2 */ + } + + /* Store the scaled-down output */ + out[base + o0] = result >> kernel[k]; +} + +static void pixel_convolve_5x5(const uint8_t *in[5], int i0, int i1, int i2, int i3, int i4, + uint8_t *out, int o0, uint16_t base, const uint16_t *kernel) +{ + int16_t result = 0; + int k = 0; + + /* Apply the coefficients on 5 rows */ + for (int h = 0; h < 5; h++) { + /* Apply the coefficients on 5 columns */ + result += in[h][base + i0] * kernel[k++]; /* line h column 0 */ + result += in[h][base + i1] * kernel[k++]; /* line h column 1 */ + result += in[h][base + i2] * kernel[k++]; /* line h column 2 */ + result += in[h][base + i3] * kernel[k++]; /* line h column 3 */ + result += in[h][base + i4] * kernel[k++]; /* line h column 4 */ + } + + /* Store the scaled-down output */ + out[base + o0] = result >> kernel[k]; +} + +/* + * Median kernels: find the median value of the input block and send it as output. The effect is to + * denoise the input image while preserving sharpness of the large color regions. + */ + +static inline uint8_t pixel_median(const uint8_t **in, int *idx, uint8_t size) +{ + uint8_t pivot_bot = 0x00; + uint8_t pivot_top = 0xff; + uint8_t num_higher; + int16_t median; + + /* Binary-search of the appropriate median value, 8 steps for 8-bit depth */ + for (int i = 0; i < 8; i++) { + num_higher = 0; + median = (pivot_top + pivot_bot) / 2; + + for (uint16_t h = 0; h < size; h++) { + for (uint16_t w = 0; w < size; w++) { + num_higher += in[h][idx[w]] > median; /* line h column w */ + } + } + + if (num_higher > size * size / 2) { + pivot_bot = median; + } else if (num_higher < size * size / 2) { + pivot_top = median; + } + } + + /* Output the median value */ + return (pivot_top + pivot_bot) / 2; +} + +static void pixel_median_3x3(const uint8_t *in[3], int i0, int i1, int i2, + uint8_t *out, int o0, uint16_t base, const uint16_t *unused) +{ + int idx[] = {base + i0, base + i1, base + i2}; + + out[base + o0] = pixel_median(in, idx, 3); +} + +static void pixel_median_5x5(const uint8_t *in[5], int i0, int i1, int i2, int i3, int i4, + uint8_t *out, int o0, uint16_t base, const uint16_t *unused) +{ + int idx[] = {base + i0, base + i1, base + i2, base + i3, base + i4}; + + out[base + o0] = pixel_median(in, idx, 5); +} + +/* + * Convert pixel offsets into byte offset, and repeat a kernel function for every channel of a + * pixel format. + */ + +static void pixel_kernel_rgb24_3x3(const uint8_t *in[3], int i0, int i1, int i2, + uint8_t *out, int o0, uint16_t base, kernel_3x3_t *line_fn, + const uint16_t *kernel) +{ + i0 *= 3, i1 *= 3, i2 *= 3, o0 *= 3, base *= 3; + line_fn(in, i0, i1, i2, out, o0, base + 0, kernel); /* R */ + line_fn(in, i0, i1, i2, out, o0, base + 1, kernel); /* G */ + line_fn(in, i0, i1, i2, out, o0, base + 2, kernel); /* B */ +} + +static void pixel_kernel_rgb24_5x5(const uint8_t *in[5], int i0, int i1, int i2, int i3, int i4, + uint8_t *out, int o0, uint16_t base, kernel_5x5_t *line_fn, + const uint16_t *kernel) +{ + i0 *= 3, i1 *= 3, i2 *= 3, i3 *= 3, i4 *= 3, o0 *= 3, base *= 3; + line_fn(in, i0, i1, i2, i3, i4, out, o0, base + 0, kernel); /* R */ + line_fn(in, i0, i1, i2, i3, i4, out, o0, base + 1, kernel); /* G */ + line_fn(in, i0, i1, i2, i3, i4, out, o0, base + 2, kernel); /* B */ +} + +/* + * Portable/default C implementation of line processing functions. They are inlined into + * line-conversion functions at the bottom of this file declared as __weak. + */ + +static inline void pixel_kernel_3x3(const uint8_t *in[3], uint8_t *out, uint16_t width, + pixfmt_3x3_t *pixfmt_fn, kernel_3x3_t *line_fn, + const uint16_t *kernel) +{ + uint16_t w = 0; + + /* Edge case on first two columns */ + pixfmt_fn(in, 0, 0, 1, out, 0, w + 0, line_fn, kernel); + + /* process the entire line except the first two and last two columns (edge cases) */ + for (w = 0; w + 3 <= width; w++) { + pixfmt_fn(in, 0, 1, 2, out, 1, w, line_fn, kernel); + } + + /* Edge case on last two columns */ + pixfmt_fn(in, 0, 1, 1, out, 1, w, line_fn, kernel); +} + +static inline void pixel_kernel_5x5(const uint8_t *in[5], uint8_t *out, uint16_t width, + pixfmt_5x5_t *pixfmt_fn, kernel_5x5_t *line_fn, + const uint16_t *kernel) +{ + uint16_t w = 0; + + /* Edge case on first two columns, repeat the left column to fill the blank */ + pixfmt_fn(in, 0, 0, 0, 1, 2, out, 0, w, line_fn, kernel); + pixfmt_fn(in, 0, 0, 1, 2, 3, out, 1, w, line_fn, kernel); + + /* process the entire line except the first two and last two columns (edge cases) */ + for (w = 0; w + 5 <= width; w++) { + pixfmt_fn(in, 0, 1, 2, 3, 4, out, 2, w, line_fn, kernel); + } + + /* Edge case on last two columns, repeat the right column to fill the blank */ + pixfmt_fn(in, 0, 1, 2, 3, 3, out, 2, w, line_fn, kernel); + pixfmt_fn(in, 1, 2, 3, 3, 3, out, 3, w, line_fn, kernel); +} + +/* + * Call a line-processing function on every line, handling the edge-cases on first line and last + * line by repeating the lines at the edge to fill the gaps. + */ + +void pixel_kernel_3x3_op(struct pixel_operation *op) +{ + uint16_t prev_line_offset = op->line_offset; + line_3x3_t *line_fn = op->arg; + const uint8_t *in[] = { + pixel_operation_get_input_line(op), + pixel_operation_peek_input_line(op), + pixel_operation_peek_input_line(op), + }; + + __ASSERT_NO_MSG(op->width >= 3); + __ASSERT_NO_MSG(op->height >= 3); + + /* Allow overflowing before the top by repeating the first line */ + if (prev_line_offset == 0) { + const uint8_t *top[] = {in[0], in[0], in[1]}; + + line_fn(top, pixel_operation_get_output_line(op), op->width); + pixel_operation_done(op); + } + + /* Process one more line */ + line_fn(in, pixel_operation_get_output_line(op), op->width); + pixel_operation_done(op); + + /* Allow overflowing after the bottom by repeating the last line */ + if (prev_line_offset + 3 >= op->height) { + const uint8_t *bot[] = {in[1], in[2], in[2]}; + + line_fn(bot, pixel_operation_get_output_line(op), op->width); + pixel_operation_done(op); + + /* Flush the remaining lines that were used for lookahead context */ + pixel_operation_get_input_line(op); + pixel_operation_get_input_line(op); + } +} + +void pixel_kernel_5x5_op(struct pixel_operation *op) +{ + uint16_t prev_line_offset = op->line_offset; + line_5x5_t *line_fn = op->arg; + const uint8_t *in[] = { + pixel_operation_get_input_line(op), + pixel_operation_peek_input_line(op), + pixel_operation_peek_input_line(op), + pixel_operation_peek_input_line(op), + pixel_operation_peek_input_line(op), + }; + + __ASSERT_NO_MSG(op->width >= 5); + __ASSERT_NO_MSG(op->height >= 5); + + /* Allow overflowing before the top by repeating the first line */ + if (prev_line_offset == 0) { + const uint8_t *top[] = {in[0], in[0], in[0], in[1], in[2], in[3]}; + + line_fn(&top[0], pixel_operation_get_output_line(op), op->width); + pixel_operation_done(op); + + line_fn(&top[1], pixel_operation_get_output_line(op), op->width); + pixel_operation_done(op); + } + + /* Process one more line */ + line_fn(in, pixel_operation_get_output_line(op), op->width); + pixel_operation_done(op); + + /* Allow overflowing after the bottom by repeating the last line */ + if (prev_line_offset + 5 >= op->height) { + const uint8_t *bot[] = {in[1], in[2], in[3], in[4], in[4], in[4]}; + + line_fn(&bot[0], pixel_operation_get_output_line(op), op->width); + pixel_operation_done(op); + + line_fn(&bot[1], pixel_operation_get_output_line(op), op->width); + pixel_operation_done(op); + + /* Flush the remaining lines that were used for lookahead context */ + pixel_operation_get_input_line(op); + pixel_operation_get_input_line(op); + pixel_operation_get_input_line(op); + pixel_operation_get_input_line(op); + } +} + +/* + * Declaration of convolution kernels, with the line-processing functions declared as __weak to + * allow them to be replaced with optimized versions + */ + +static const int16_t pixel_identity_3x3[] = { + 0, 0, 0, + 0, 1, 0, + 0, 0, 0, 0 +}; + +__weak void pixel_identity_rgb24_3x3(const uint8_t *in[3], uint8_t *out, uint16_t width) +{ + pixel_kernel_3x3(in, out, width, pixel_kernel_rgb24_3x3, pixel_convolve_3x3, + pixel_identity_3x3); +} +PIXEL_DEFINE_KERNEL_3X3_OPERATION(pixel_identity_rgb24_3x3, + PIXEL_KERNEL_IDENTITY, VIDEO_PIX_FMT_RGB24); + +static const int16_t pixel_identity_5x5[] = { + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 +}; + +__weak void pixel_identity_rgb24_5x5(const uint8_t *in[5], uint8_t *out, uint16_t width) +{ + pixel_kernel_5x5(in, out, width, pixel_kernel_rgb24_5x5, pixel_convolve_5x5, + pixel_identity_5x5); +} +PIXEL_DEFINE_KERNEL_5X5_OPERATION(pixel_identity_rgb24_5x5, + PIXEL_KERNEL_IDENTITY, VIDEO_PIX_FMT_RGB24); + +static const int16_t pixel_edgedetect_3x3[] = { + -1, -1, -1, + -1, 8, -1, + -1, -1, -1, 0 +}; + +__weak void pixel_edgedetect_rgb24_3x3(const uint8_t *in[3], uint8_t *out, uint16_t width) +{ + pixel_kernel_3x3(in, out, width, pixel_kernel_rgb24_3x3, pixel_convolve_3x3, + pixel_edgedetect_3x3); +} +PIXEL_DEFINE_KERNEL_3X3_OPERATION(pixel_edgedetect_rgb24_3x3, + PIXEL_KERNEL_EDGE_DETECT, VIDEO_PIX_FMT_RGB24); + +static const int16_t pixel_gaussianblur_3x3[] = { + 1, 2, 1, + 2, 4, 2, + 1, 2, 1, 4 +}; + +__weak void pixel_gaussianblur_rgb24_3x3(const uint8_t *in[3], uint8_t *out, uint16_t width) +{ + pixel_kernel_3x3(in, out, width, pixel_kernel_rgb24_3x3, pixel_convolve_3x3, + pixel_gaussianblur_3x3); +} +PIXEL_DEFINE_KERNEL_3X3_OPERATION(pixel_gaussianblur_rgb24_3x3, + PIXEL_KERNEL_GAUSSIAN_BLUR, VIDEO_PIX_FMT_RGB24); + +static const int16_t pixel_gaussianblur_5x5[] = { + 1, 4, 6, 4, 1, + 4, 16, 24, 16, 4, + 6, 24, 36, 24, 6, + 4, 16, 24, 16, 4, + 1, 4, 6, 4, 1, 8 +}; + +__weak void pixel_gaussianblur_rgb24_5x5(const uint8_t *in[5], uint8_t *out, uint16_t width) +{ + pixel_kernel_5x5(in, out, width, pixel_kernel_rgb24_5x5, pixel_convolve_5x5, + pixel_gaussianblur_5x5); +} +PIXEL_DEFINE_KERNEL_5X5_OPERATION(pixel_gaussianblur_rgb24_5x5, + PIXEL_KERNEL_GAUSSIAN_BLUR, VIDEO_PIX_FMT_RGB24); + +static const int16_t pixel_sharpen_3x3[] = { + 0, -1, 0, + -1, 5, -1, + 0, -1, 0, 0 +}; + +__weak void pixel_sharpen_rgb24_3x3(const uint8_t *in[3], uint8_t *out, uint16_t width) +{ + pixel_kernel_3x3(in, out, width, pixel_kernel_rgb24_3x3, pixel_convolve_3x3, + pixel_sharpen_3x3); +} +PIXEL_DEFINE_KERNEL_3X3_OPERATION(pixel_sharpen_rgb24_3x3, + PIXEL_KERNEL_SHARPEN, VIDEO_PIX_FMT_RGB24); + +static const int16_t pixel_unsharp_5x5[] = { + -1, -4, -6, -4, -1, + -4, -16, -24, -16, -4, + -6, -24, 476, -24, -6, + -4, -16, -24, -16, -4, + -1, -4, -6, -4, -1, 8 +}; + +__weak void pixel_sharpen_rgb24_5x5(const uint8_t *in[5], uint8_t *out, uint16_t width) +{ + pixel_kernel_5x5(in, out, width, pixel_kernel_rgb24_5x5, pixel_convolve_5x5, + pixel_unsharp_5x5); +} +PIXEL_DEFINE_KERNEL_5X5_OPERATION(pixel_sharpen_rgb24_5x5, + PIXEL_KERNEL_SHARPEN, VIDEO_PIX_FMT_RGB24); + +/* + * Declaration of median kernels, with the line-processing functions declared as __weak to + * allow them to be replaced with optimized versions + */ + +__weak void pixel_median_rgb24_5x5(const uint8_t *in[5], uint8_t *out, uint16_t width) +{ + pixel_kernel_5x5(in, out, width, pixel_kernel_rgb24_5x5, pixel_median_5x5, NULL); +} + +PIXEL_DEFINE_KERNEL_5X5_OPERATION(pixel_median_rgb24_5x5, + PIXEL_KERNEL_DENOISE, VIDEO_PIX_FMT_RGB24); + +__weak void pixel_median_rgb24_3x3(const uint8_t *in[3], uint8_t *out, uint16_t width) +{ + pixel_kernel_3x3(in, out, width, pixel_kernel_rgb24_3x3, pixel_median_3x3, NULL); +} +PIXEL_DEFINE_KERNEL_3X3_OPERATION(pixel_median_rgb24_3x3, + PIXEL_KERNEL_DENOISE, VIDEO_PIX_FMT_RGB24); diff --git a/lib/pixel/pixel.ld b/lib/pixel/pixel.ld new file mode 100644 index 0000000000000..0a606969bf4e2 --- /dev/null +++ b/lib/pixel/pixel.ld @@ -0,0 +1,8 @@ +# Copyright The Zephyr Project Contributors +# SPDX-License-Identifier: Apache-2.0 + +#include + +ITERABLE_SECTION_RAM(pixel_convert, Z_LINK_ITERABLE_SUBALIGN) +ITERABLE_SECTION_RAM(pixel_resize, Z_LINK_ITERABLE_SUBALIGN) +ITERABLE_SECTION_RAM(pixel_kernel, Z_LINK_ITERABLE_SUBALIGN) diff --git a/lib/pixel/print.c b/lib/pixel/print.c new file mode 100644 index 0000000000000..fdd2c88fa263b --- /dev/null +++ b/lib/pixel/print.c @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include + +#ifdef CONFIG_PIXEL_PRINT_NONE +#define PIXEL_PRINT(...) +#endif + +#ifdef CONFIG_PIXEL_PRINT_PRINTF +#define PIXEL_PRINT(...) printf(__VA_ARGS__) +#endif + +#ifdef CONFIG_PIXEL_PRINT_PRINTK +#define PIXEL_PRINT(...) printk(__VA_ARGS__) +#endif + +#ifdef CONFIG_PIXEL_PRINT_SHELL +#define PIXEL_PRINT(...) shell_print(pixel_print_shell, __VA_ARGS__) +#endif + +static struct shell *pixel_print_shell; + +void pixel_print_set_shell(struct shell *sh) +{ + pixel_print_shell = sh; +} + +__unused static uint8_t pixel_rgb24_to_256color(const uint8_t rgb24[3]) +{ + return 16 + rgb24[0] * 6 / 256 * 36 + rgb24[1] * 6 / 256 * 6 + rgb24[2] * 6 / 256 * 1; +} + +__unused static uint8_t pixel_gray8_to_256color(uint8_t gray8) +{ + return 232 + gray8 * 24 / 256; +} + +static void pixel_print_truecolor(const uint8_t rgb24row0[3], const uint8_t rgb24row1[3]) +{ + PIXEL_PRINT("\e[48;2;%u;%u;%um\e[38;2;%u;%u;%um▄", + rgb24row0[0], rgb24row0[1], rgb24row0[2], + rgb24row1[0], rgb24row1[1], rgb24row1[2]); +} + +static void pixel_print_256color(const uint8_t rgb24row0[3], const uint8_t rgb24row1[3]) +{ + PIXEL_PRINT("\e[48;5;%um\e[38;5;%um▄", + pixel_rgb24_to_256color(rgb24row0), + pixel_rgb24_to_256color(rgb24row1)); +} + +static void pixel_print_256gray(uint8_t gray8row0, uint8_t gray8row1) +{ + PIXEL_PRINT("\e[48;5;%um\e[38;5;%um▄", + pixel_gray8_to_256color(gray8row0), + pixel_gray8_to_256color(gray8row1)); +} + +typedef void fn_print_t(const uint8_t rgb24row0[3], const uint8_t rgb24row1[3]); + +typedef void fn_conv_t(const uint8_t *src, uint8_t *dst, uint16_t w); + +static void pixel_print(const uint8_t *src, size_t size, uint16_t width, uint16_t height, + fn_print_t *fn_print, fn_conv_t *fn_conv, int bitspp, int npix) +{ + size_t pitch = width * bitspp / BITS_PER_BYTE; + uint8_t nbytes = npix * bitspp / BITS_PER_BYTE; + + for (size_t i = 0, h = 0; h + 2 <= height; h += 2) { + for (size_t w = 0; w + npix <= width; w += npix, i += nbytes) { + uint8_t rgb24a[3 * 2], rgb24b[3 * 2]; + + __ASSERT_NO_MSG(npix <= 2); + + fn_conv(&src[i + pitch * 0], rgb24a, npix); + fn_conv(&src[i + pitch * 1], rgb24b, npix); + + if (i + pitch > size) { + PIXEL_PRINT("\e[m *** early end of buffer at %zu bytes ***\n", + size); + return; + } + + for (int n = 0; n < npix; n++) { + fn_print(&rgb24a[n * 3], &rgb24b[n * 3]); + } + } + PIXEL_PRINT("\e[m|\n"); + + /* Skip the second h being printed at the same time */ + i += pitch; + } +} + +static void pixel_print_buffer(const uint8_t *buffer, size_t size, uint16_t width, uint16_t height, + uint32_t fourcc, fn_print_t *fn) +{ + switch (fourcc) { + case VIDEO_PIX_FMT_RGB24: + pixel_print(buffer, size, width, height, fn, pixel_line_rgb24_to_rgb24, + video_bits_per_pixel(fourcc), 1); + break; + case VIDEO_PIX_FMT_RGB565: + pixel_print(buffer, size, width, height, fn, pixel_line_rgb565le_to_rgb24, + video_bits_per_pixel(fourcc), 1); + break; + case VIDEO_PIX_FMT_RGB565X: + pixel_print(buffer, size, width, height, fn, pixel_line_rgb565be_to_rgb24, + video_bits_per_pixel(fourcc), 1); + break; + case VIDEO_PIX_FMT_RGB332: + pixel_print(buffer, size, width, height, fn, pixel_line_rgb332_to_rgb24, + video_bits_per_pixel(fourcc), 1); + break; + case VIDEO_PIX_FMT_YUYV: + pixel_print(buffer, size, width, height, fn, pixel_line_yuyv_to_rgb24_bt709, + video_bits_per_pixel(fourcc), 2); + break; + case VIDEO_PIX_FMT_YUV24: + pixel_print(buffer, size, width, height, fn, pixel_line_yuv24_to_rgb24_bt709, + video_bits_per_pixel(fourcc), 1); + break; + case VIDEO_PIX_FMT_RGGB8: + case VIDEO_PIX_FMT_BGGR8: + case VIDEO_PIX_FMT_GBRG8: + case VIDEO_PIX_FMT_GRBG8: + case VIDEO_PIX_FMT_GREY: + pixel_print(buffer, size, width, height, fn, pixel_line_y8_to_rgb24_bt709, + video_bits_per_pixel(fourcc), 1); + break; + default: + PIXEL_PRINT("Printing %s buffers not supported\n", VIDEO_FOURCC_TO_STR(fourcc)); + } +} + +void pixel_print_buffer_truecolor(const uint8_t *buffer, size_t size, uint16_t width, + uint16_t height, uint32_t fourcc) +{ + pixel_print_buffer(buffer, size, width, height, fourcc, pixel_print_truecolor); +} + +void pixel_print_buffer_256color(const uint8_t *buffer, size_t size, uint16_t width, + uint16_t height, uint32_t fourcc) +{ + pixel_print_buffer(buffer, size, width, height, fourcc, pixel_print_256color); +} + +void pixel_image_print_truecolor(struct pixel_image *img) +{ + pixel_print_buffer_truecolor(img->buffer, img->size, img->width, img->height, img->fourcc); +} + +void pixel_image_print_256color(struct pixel_image *img) +{ + pixel_print_buffer_256color(img->buffer, img->size, img->width, img->height, img->fourcc); +} + +void pixel_hexdump_raw8(const uint8_t *raw8, size_t size, uint16_t width, uint16_t height) +{ + for (uint16_t h = 0; h < height; h++) { + for (uint16_t w = 0; w < width; w++) { + size_t i = h * width * 1 + w * 1; + + if (i >= size) { + PIXEL_PRINT("\e[m *** early end of buffer at %zu bytes ***\n", + size); + return; + } + + PIXEL_PRINT(" %02x", raw8[i]); + } + PIXEL_PRINT(" row%u\n", h); + } +} + +void pixel_hexdump_rgb24(const uint8_t *rgb24, size_t size, uint16_t width, uint16_t height) +{ + PIXEL_PRINT(" "); + for (uint16_t w = 0; w < width; w++) { + PIXEL_PRINT("col%-7u", w); + } + PIXEL_PRINT("\n"); + + for (uint16_t w = 0; w < width; w++) { + PIXEL_PRINT(" R G B "); + } + PIXEL_PRINT("\n"); + + for (uint16_t h = 0; h < height; h++) { + for (uint16_t w = 0; w < width; w++) { + size_t i = h * width * 3 + w * 3; + + if (i + 2 >= size) { + PIXEL_PRINT("\e[m *** early end of buffer at %zu bytes ***\n", + size); + return; + } + + PIXEL_PRINT(" %02x %02x %02x ", rgb24[i + 0], rgb24[i + 1], rgb24[i + 2]); + } + PIXEL_PRINT(" row%u\n", h); + } +} + +void pixel_hexdump_rgb565(const uint8_t *rgb565, size_t size, uint16_t width, uint16_t height) +{ + PIXEL_PRINT(" "); + for (uint16_t w = 0; w < width; w++) { + PIXEL_PRINT("col%-4u", w); + } + PIXEL_PRINT("\n"); + + for (uint16_t w = 0; w < width; w++) { + PIXEL_PRINT(" RGB565"); + } + PIXEL_PRINT("\n"); + + for (uint16_t h = 0; h < height; h++) { + for (uint16_t w = 0; w < width; w++) { + size_t i = h * width * 2 + w * 2; + + if (i + 1 >= size) { + PIXEL_PRINT("\e[m *** early end of buffer at %zu bytes ***\n", + size); + return; + } + + PIXEL_PRINT(" %02x %02x ", rgb565[i + 0], rgb565[i + 1]); + } + PIXEL_PRINT(" row%u\n", h); + } +} + +void pixel_hexdump_yuyv(const uint8_t *yuyv, size_t size, uint16_t width, uint16_t height) +{ + PIXEL_PRINT(" "); + for (uint16_t w = 0; w < width; w++) { + PIXEL_PRINT("col%-3u", w); + if ((w + 1) % 2 == 0) { + PIXEL_PRINT(" "); + } + } + PIXEL_PRINT("\n"); + + for (uint16_t w = 0; w < width; w++) { + PIXEL_PRINT(" %c%u", "YUYV"[w % 2 * 2 + 0], w % 2); + PIXEL_PRINT(" %c%u", "YUYV"[w % 2 * 2 + 1], w % 2); + if ((w + 1) % 2 == 0) { + PIXEL_PRINT(" "); + } + } + PIXEL_PRINT("\n"); + + for (uint16_t h = 0; h < height; h++) { + for (uint16_t w = 0; w < width; w++) { + size_t i = h * width * 2 + w * 2; + + if (i + 1 >= size) { + PIXEL_PRINT("\e[m *** early end of buffer at %zu bytes ***\n", + size); + return; + } + + PIXEL_PRINT(" %02x %02x", yuyv[i], yuyv[i + 1]); + if ((w + 1) % 2 == 0) { + PIXEL_PRINT(" "); + } + } + PIXEL_PRINT(" row%u\n", h); + } +} + +static void pixel_print_hist_scale(size_t size) +{ + for (uint16_t i = 0; i < size; i++) { + pixel_print_256gray(0, i * 256 / size); + } + PIXEL_PRINT("\e[m\n"); +} + +void pixel_print_rgb24hist(const uint16_t *rgb24hist, size_t size, uint16_t height) +{ + const uint16_t *r8hist = &rgb24hist[size / 3 * 0]; + const uint16_t *g8hist = &rgb24hist[size / 3 * 1]; + const uint16_t *b8hist = &rgb24hist[size / 3 * 2]; + uint32_t max = 1; + + __ASSERT(size % 3 == 0, "Each of R, G, B channel should have the same size."); + + for (size_t i = 0; i < size; i++) { + max = rgb24hist[i] > max ? rgb24hist[i] : max; + } + + for (uint16_t h = height; h > 1; h--) { + for (size_t i = 0; i < size / 3; i++) { + uint8_t rgb24row0[3]; + uint8_t rgb24row1[3]; + + rgb24row0[0] = (r8hist[i] * height / max > h - 0) ? 0xff : 0x00; + rgb24row0[1] = (g8hist[i] * height / max > h - 0) ? 0xff : 0x00; + rgb24row0[2] = (b8hist[i] * height / max > h - 0) ? 0xff : 0x00; + rgb24row1[0] = (r8hist[i] * height / max > h - 1) ? 0xff : 0x00; + rgb24row1[1] = (g8hist[i] * height / max > h - 1) ? 0xff : 0x00; + rgb24row1[2] = (b8hist[i] * height / max > h - 1) ? 0xff : 0x00; + + pixel_print_256color(rgb24row0, rgb24row1); + } + PIXEL_PRINT("\e[m| - %u\n", h * max / height); + } + + pixel_print_hist_scale(size / 3); +} + +void pixel_print_y8hist(const uint16_t *y8hist, size_t size, uint16_t height) +{ + uint32_t max = 1; + + for (size_t i = 0; i < size; i++) { + max = y8hist[i] > max ? y8hist[i] : max; + } + + for (uint16_t h = height; h > 1; h--) { + for (size_t i = 0; i < size; i++) { + uint8_t gray8row0 = (y8hist[i] * height / max > h - 0) ? 0xff : 0x00; + uint8_t gray8row1 = (y8hist[i] * height / max > h - 1) ? 0xff : 0x00; + + pixel_print_256gray(gray8row0, gray8row1); + } + PIXEL_PRINT("\e[m| - %u\n", h * max / height); + } + + pixel_print_hist_scale(size); +} diff --git a/lib/pixel/resize.c b/lib/pixel/resize.c new file mode 100644 index 0000000000000..7675da5392c81 --- /dev/null +++ b/lib/pixel/resize.c @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include + +LOG_MODULE_REGISTER(pixel_resize, CONFIG_PIXEL_LOG_LEVEL); + +int pixel_image_resize(struct pixel_image *img, uint16_t width, uint16_t height) +{ + const struct pixel_operation *op = NULL; + int ret; + + STRUCT_SECTION_FOREACH_ALTERNATE(pixel_resize, pixel_operation, tmp) { + if (tmp->fourcc_in == img->fourcc) { + op = tmp; + break; + } + } + + if (op == NULL) { + LOG_ERR("Resize operation for %s not found", VIDEO_FOURCC_TO_STR(img->fourcc)); + return pixel_image_error(img, -ENOSYS); + } + + ret = pixel_image_add_uncompressed(img, op); + img->width = width; + img->height = height; + return ret; +} + +static inline void pixel_resize_line(const uint8_t *src_buf, size_t src_width, uint8_t *dst_buf, + size_t dst_width, uint8_t bits_per_pixel) +{ + for (size_t dst_w = 0; dst_w < dst_width; dst_w++) { + size_t src_w = dst_w * src_width / dst_width; + size_t src_i = src_w * bits_per_pixel / BITS_PER_BYTE; + size_t dst_i = dst_w * bits_per_pixel / BITS_PER_BYTE; + + memmove(&dst_buf[dst_i], &src_buf[src_i], bits_per_pixel / BITS_PER_BYTE); + } +} + +static inline void pixel_resize_frame(const uint8_t *src_buf, size_t src_width, size_t src_height, + uint8_t *dst_buf, size_t dst_width, size_t dst_height, + uint8_t bits_per_pixel) +{ + for (size_t dst_h = 0; dst_h < dst_height; dst_h++) { + size_t src_h = dst_h * src_height / dst_height; + size_t src_i = src_h * src_width * bits_per_pixel / BITS_PER_BYTE; + size_t dst_i = dst_h * dst_width * bits_per_pixel / BITS_PER_BYTE; + + pixel_resize_line(&src_buf[src_i], src_width, &dst_buf[dst_i], dst_width, + bits_per_pixel); + } +} + +__weak void pixel_resize_frame_raw24(const uint8_t *src_buf, size_t src_width, size_t src_height, + uint8_t *dst_buf, size_t dst_width, size_t dst_height) +{ + pixel_resize_frame(src_buf, src_width, src_height, dst_buf, dst_width, dst_height, 24); +} + +__weak void pixel_resize_frame_raw16(const uint8_t *src_buf, size_t src_width, size_t src_height, + uint8_t *dst_buf, size_t dst_width, size_t dst_height) +{ + pixel_resize_frame(src_buf, src_width, src_height, dst_buf, dst_width, dst_height, 16); +} + +__weak void pixel_resize_frame_raw8(const uint8_t *src_buf, size_t src_width, size_t src_height, + uint8_t *dst_buf, size_t dst_width, size_t dst_height) +{ + pixel_resize_frame(src_buf, src_width, src_height, dst_buf, dst_width, dst_height, 8); +} + +static inline void pixel_op_resize(struct pixel_operation *op, uint8_t bits_per_pixel) +{ + struct pixel_operation *next = SYS_SLIST_PEEK_NEXT_CONTAINER(op, node); + uint16_t prev_offset = (op->line_offset + 1) * next->height / op->height; + const uint8_t *line_in = pixel_operation_get_input_line(op); + uint16_t next_offset = (op->line_offset + 1) * next->height / op->height; + + for (uint16_t i = 0; prev_offset + i < next_offset; i++) { + pixel_resize_line(line_in, op->width, pixel_operation_get_output_line(op), + next->width, bits_per_pixel); + pixel_operation_done(op); + } +} + +__weak void pixel_op_resize_raw32(struct pixel_operation *op) +{ + pixel_op_resize(op, 32); +} +PIXEL_DEFINE_RESIZE_OPERATION(pixel_op_resize_raw32, VIDEO_PIX_FMT_ABGR32); +PIXEL_DEFINE_RESIZE_OPERATION(pixel_op_resize_raw32, VIDEO_PIX_FMT_ARGB32); +PIXEL_DEFINE_RESIZE_OPERATION(pixel_op_resize_raw32, VIDEO_PIX_FMT_BGRA32); +PIXEL_DEFINE_RESIZE_OPERATION(pixel_op_resize_raw32, VIDEO_PIX_FMT_RGBA32); +PIXEL_DEFINE_RESIZE_OPERATION(pixel_op_resize_raw32, VIDEO_PIX_FMT_XRGB32); +PIXEL_DEFINE_RESIZE_OPERATION(pixel_op_resize_raw32, VIDEO_PIX_FMT_XYUV32); + +__weak void pixel_op_resize_raw24(struct pixel_operation *op) +{ + pixel_op_resize(op, 24); +} +PIXEL_DEFINE_RESIZE_OPERATION(pixel_op_resize_raw24, VIDEO_PIX_FMT_BGR24); +PIXEL_DEFINE_RESIZE_OPERATION(pixel_op_resize_raw24, VIDEO_PIX_FMT_RGB24); +PIXEL_DEFINE_RESIZE_OPERATION(pixel_op_resize_raw24, VIDEO_PIX_FMT_YUV24); + +__weak void pixel_op_resize_raw16(struct pixel_operation *op) +{ + pixel_op_resize(op, 16); +} +PIXEL_DEFINE_RESIZE_OPERATION(pixel_op_resize_raw16, VIDEO_PIX_FMT_RGB565); +PIXEL_DEFINE_RESIZE_OPERATION(pixel_op_resize_raw16, VIDEO_PIX_FMT_RGB565X); + +__weak void pixel_op_resize_raw8(struct pixel_operation *op) +{ + pixel_op_resize(op, 8); +} +PIXEL_DEFINE_RESIZE_OPERATION(pixel_op_resize_raw8, VIDEO_PIX_FMT_GREY); +PIXEL_DEFINE_RESIZE_OPERATION(pixel_op_resize_raw8, VIDEO_PIX_FMT_RGB332); diff --git a/lib/pixel/stats.c b/lib/pixel/stats.c new file mode 100644 index 0000000000000..a63f716b7e879 --- /dev/null +++ b/lib/pixel/stats.c @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include + +#define PIXEL_IDX_R 0 +#define PIXEL_IDX_G 1 +#define PIXEL_IDX_B 2 + +static const uint8_t pixel_idx_rggb8[4] = {PIXEL_IDX_R, PIXEL_IDX_G, PIXEL_IDX_G, PIXEL_IDX_B}; +static const uint8_t pixel_idx_bggr8[4] = {PIXEL_IDX_B, PIXEL_IDX_G, PIXEL_IDX_G, PIXEL_IDX_R}; +static const uint8_t pixel_idx_gbrg8[4] = {PIXEL_IDX_G, PIXEL_IDX_B, PIXEL_IDX_R, PIXEL_IDX_G}; +static const uint8_t pixel_idx_grbg8[4] = {PIXEL_IDX_G, PIXEL_IDX_R, PIXEL_IDX_B, PIXEL_IDX_G}; + +/* Extract a random value from the buffer */ + +static inline uint32_t pixel_rand(void) +{ + static uint32_t lcg_state; + + /* Linear Congruent Generator (LCG) are low-quality but very fast, here considered enough + * as even a fixed offset would have been enough.The % phase is skipped as there is already + * "% vbuf->bytesused" downstream in the code. + * + * The constants are from https://en.wikipedia.org/wiki/Linear_congruential_generator + */ + lcg_state = lcg_state * 1103515245 + 12345; + return lcg_state; +} + +static inline void pixel_sample_rgb24(const uint8_t *buf, size_t size, uint8_t rgb24[3]) +{ + uint32_t pos = pixel_rand() % size; + + /* Align on 24-bit pixel boundary */ + pos -= pos % 3; + + rgb24[0] = buf[pos + 0]; + rgb24[1] = buf[pos + 1]; + rgb24[2] = buf[pos + 2]; +} + +static inline void pixel_sample_bayer(const uint8_t *buf, size_t size, uint16_t width, + uint8_t rgb24[3], const uint8_t *idx) +{ + uint32_t pos = pixel_rand() % size; + + /* Make sure to be on even row and column position */ + pos -= pos % 2; + pos -= pos / width % 2 * width; + + rgb24[idx[0]] = buf[pos + 0]; + rgb24[idx[1]] = buf[pos + 1]; + rgb24[idx[2]] = buf[pos + width + 0]; + rgb24[idx[3]] = buf[pos + width + 1]; +} + +static inline void pixel_sums_to_rgb24avg(uint32_t sums[3], uint8_t rgb24avg[3], uint16_t nval) +{ + rgb24avg[0] = sums[0] / nval; + rgb24avg[1] = sums[1] / nval; + rgb24avg[2] = sums[2] / nval; +} + +static inline void pixel_sums_add_rgb24(uint32_t sums[3], uint8_t rgb24[3]) +{ + sums[0] += rgb24[0], sums[1] += rgb24[1]; + sums[2] += rgb24[2]; +} + +/* Channel average statistics */ + +void pixel_rgb24frame_to_rgb24avg(const uint8_t *buf, size_t size, uint8_t rgb24avg[3], + uint16_t nval) +{ + uint32_t sums[3] = {0, 0, 0}; + uint8_t rgb24[3]; + + for (uint16_t n = 0; n < nval; n++) { + pixel_sample_rgb24(buf, size, rgb24); + pixel_sums_add_rgb24(sums, rgb24); + } + pixel_sums_to_rgb24avg(sums, rgb24avg, nval); +} + +static inline void pixel_bayerframe_to_rgb24avg(const uint8_t *buf, size_t size, uint16_t width, + uint8_t rgb24avg[3], uint16_t nval, + const uint8_t *idx) +{ + uint32_t sums[3] = {0, 0, 0}; + uint8_t rgb24[3]; + + for (uint16_t n = 0; n < nval; n++) { + pixel_sample_bayer(buf, size, width, rgb24, idx); + pixel_sums_add_rgb24(sums, rgb24); + } + pixel_sums_to_rgb24avg(sums, rgb24avg, nval); +} + +void pixel_rggb8frame_to_rgb24avg(const uint8_t *buf, size_t size, uint16_t width, + uint8_t rgb24avg[3], uint16_t nval) +{ + pixel_bayerframe_to_rgb24avg(buf, size, width, rgb24avg, nval, pixel_idx_rggb8); +} + +void pixel_bggr8frame_to_rgb24avg(const uint8_t *buf, size_t size, uint16_t width, + uint8_t rgb24avg[3], uint16_t nval) +{ + pixel_bayerframe_to_rgb24avg(buf, size, width, rgb24avg, nval, pixel_idx_bggr8); +} + +void pixel_gbrg8_to_rgb24avg(const uint8_t *buf, size_t size, uint16_t width, + uint8_t rgb24avg[3], uint16_t nval) +{ + pixel_bayerframe_to_rgb24avg(buf, size, width, rgb24avg, nval, pixel_idx_gbrg8); +} + +void pixel_grbg8frame_to_rgb24avg(const uint8_t *buf, size_t size, uint16_t width, + uint8_t rgb24avg[3], uint16_t nval) +{ + pixel_bayerframe_to_rgb24avg(buf, size, width, rgb24avg, nval, pixel_idx_grbg8); +} + +/* RGB24 histogram statistics */ + +static inline void pixel_rgb24hist_add_rgb24(uint16_t *rgb24hist, uint8_t rgb24[3], + uint8_t bit_depth) +{ + uint16_t *r8hist = &rgb24hist[0 * (1 << bit_depth)], r8 = rgb24[0]; + uint16_t *g8hist = &rgb24hist[1 * (1 << bit_depth)], g8 = rgb24[1]; + uint16_t *b8hist = &rgb24hist[2 * (1 << bit_depth)], b8 = rgb24[2]; + + r8hist[r8 >> (BITS_PER_BYTE - bit_depth)]++; + g8hist[g8 >> (BITS_PER_BYTE - bit_depth)]++; + b8hist[b8 >> (BITS_PER_BYTE - bit_depth)]++; +} + +void pixel_rgb24frame_to_rgb24hist(const uint8_t *buf, size_t buf_size, uint16_t *rgb24hist, + size_t hist_size, uint16_t nval) +{ + uint8_t bit_depth = LOG2(hist_size / 3); + uint8_t rgb24[3]; + + __ASSERT(hist_size % 3 == 0, "Each of R, G, B channel should have the same size."); + __ASSERT(1 << bit_depth == hist_size / 3, "Each channel size should be a power of two."); + + memset(rgb24hist, 0x00, hist_size * sizeof(*rgb24hist)); + + for (uint16_t n = 0; n < nval; n++) { + pixel_sample_rgb24(buf, buf_size, rgb24); + pixel_rgb24hist_add_rgb24(rgb24hist, rgb24, bit_depth); + } +} + +static inline void pixel_bayerframe_to_rgb24hist(const uint8_t *buf, size_t buf_size, + uint16_t width, uint16_t *rgb24hist, + size_t hist_size, uint16_t nval, + const uint8_t *idx) +{ + uint8_t bit_depth = LOG2(hist_size / 3); + uint8_t rgb24[3]; + + __ASSERT(hist_size % 3 == 0, "Each of R, G, B channel should have the same size."); + __ASSERT(1 << bit_depth == hist_size / 3, "Each channel size should be a power of two."); + + memset(rgb24hist, 0x00, hist_size * sizeof(*rgb24hist)); + + for (uint16_t n = 0; n < nval; n++) { + pixel_sample_bayer(buf, buf_size, width, rgb24, idx); + pixel_rgb24hist_add_rgb24(rgb24hist, rgb24, bit_depth); + } +} + +void pixel_rggb8frame_to_rgb24hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *rgb24hist, size_t hist_size, uint16_t nval) +{ + pixel_bayerframe_to_rgb24hist(buf, buf_size, width, rgb24hist, hist_size, nval, + pixel_idx_rggb8); +} + +void pixel_gbrg8frame_to_rgb24hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *rgb24hist, size_t hist_size, uint16_t nval) +{ + pixel_bayerframe_to_rgb24hist(buf, buf_size, width, rgb24hist, hist_size, nval, + pixel_idx_gbrg8); +} + +void pixel_bggr8frame_to_rgb24hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *rgb24hist, size_t hist_size, uint16_t nval) +{ + pixel_bayerframe_to_rgb24hist(buf, buf_size, width, rgb24hist, hist_size, nval, + pixel_idx_bggr8); +} + +void pixel_grbg8frame_to_rgb24hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *rgb24hist, size_t hist_size, uint16_t nval) +{ + pixel_bayerframe_to_rgb24hist(buf, buf_size, width, rgb24hist, hist_size, nval, + pixel_idx_grbg8); +} + +/* Y8 histogram statistics + * Use BT.709 (sRGB) as an arbitrary choice, instead of BT.601 like libcamera + */ + +static inline void pixel_y8hist_add_y8(uint16_t *y8hist, uint8_t y8, uint8_t bit_depth) +{ + y8hist[y8 >> (BITS_PER_BYTE - bit_depth)]++; +} + +void pixel_rgb24frame_to_y8hist(const uint8_t *buf, size_t buf_size, uint16_t *y8hist, + size_t hist_size, uint16_t nval) +{ + uint8_t bit_depth = LOG2(hist_size); + uint8_t rgb24[3]; + + __ASSERT(1 << bit_depth == hist_size, "Histogram channel size should be a power of two."); + + memset(y8hist, 0x00, hist_size * sizeof(*y8hist)); + + for (uint16_t n = 0; n < nval; n++) { + pixel_sample_rgb24(buf, buf_size, rgb24); + pixel_y8hist_add_y8(y8hist, pixel_rgb24_get_luma_bt709(rgb24), bit_depth); + } +} + +static inline void pixel_bayerframe_to_y8hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *y8hist, size_t hist_size, uint16_t nval, + const uint8_t *idx) +{ + uint8_t bit_depth = LOG2(hist_size); + uint8_t rgb24[3]; + + __ASSERT(1 << bit_depth == hist_size, "Histogram channel size should be a power of two."); + + memset(y8hist, 0x00, hist_size * sizeof(*y8hist)); + + for (uint16_t n = 0; n < nval; n++) { + pixel_sample_bayer(buf, buf_size, width, rgb24, idx); + pixel_y8hist_add_y8(y8hist, pixel_rgb24_get_luma_bt709(rgb24), bit_depth); + } +} + +void pixel_rggb8frame_to_y8hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *y8hist, size_t hist_size, uint16_t nval) +{ + pixel_bayerframe_to_y8hist(buf, buf_size, width, y8hist, hist_size, nval, pixel_idx_rggb8); +} + +void pixel_gbrg8frame_to_y8hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *y8hist, size_t hist_size, uint16_t nval) +{ + pixel_bayerframe_to_y8hist(buf, buf_size, width, y8hist, hist_size, nval, pixel_idx_gbrg8); +} + +void pixel_bggr8frame_to_y8hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *y8hist, size_t hist_size, uint16_t nval) +{ + pixel_bayerframe_to_y8hist(buf, buf_size, width, y8hist, hist_size, nval, pixel_idx_bggr8); +} + +void pixel_grbg8frame_to_y8hist(const uint8_t *buf, size_t buf_size, uint16_t width, + uint16_t *y8hist, size_t hist_size, uint16_t nval) +{ + pixel_bayerframe_to_y8hist(buf, buf_size, width, y8hist, hist_size, nval, pixel_idx_grbg8); +} diff --git a/samples/lib/lib.rst b/samples/lib/lib.rst new file mode 100644 index 0000000000000..89fd6e8db9cbd --- /dev/null +++ b/samples/lib/lib.rst @@ -0,0 +1,6 @@ +.. zephyr:code-sample-category:: lib + :name: Libraries + :show-listing: + :live-search: + + These samples demonstrate how to use the libraries present in Zephyr. diff --git a/samples/lib/pixel/image/CMakeLists.txt b/samples/lib/pixel/image/CMakeLists.txt new file mode 100644 index 0000000000000..daab365a4e9d2 --- /dev/null +++ b/samples/lib/pixel/image/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(pixel_image) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/lib/pixel/image/README.rst b/samples/lib/pixel/image/README.rst new file mode 100644 index 0000000000000..0126080c6fa32 --- /dev/null +++ b/samples/lib/pixel/image/README.rst @@ -0,0 +1,51 @@ +.. zephyr:code-sample:: lib_pixel_image + :name: Pixel Imaging Library + + Image an image using subsampling. + +Overview +******** + +A sample showcasing how to perform arbitrary image manipulation using few memory with a +developer-friendly API. + +The input and output are printed as preview images on the console. + +Building and Running +******************** + +This application can be built and executed on the native simulator as follows: + +.. zephyr-app-commands:: + :zephyr-app: samples/lib/pixel/image + :host-os: unix + :board: native_sim + :goals: run + :compact: + +To build for another board, change "native_sim" above to that board's name. + +Sample Output +============= + +.. code-block:: console + + *** Booting Zephyr OS build v4.1.0-3103-g25dca1b19885 *** + [00:00:00.000,000] app: input image, 32x8, 768 bytes: + ▄▄▄▄▄▄▄▄▄▄▄ shows-up ▄▄▄▄▄▄▄▄▄▄▄| + ▄▄▄▄▄▄▄▄▄▄▄ as color ▄▄▄▄▄▄▄▄▄▄▄| + ▄▄▄▄▄▄▄▄▄▄▄ on the ▄▄▄▄▄▄▄▄▄▄▄| + ▄▄▄▄▄▄▄▄▄▄▄ terminal ▄▄▄▄▄▄▄▄▄▄▄| + [00:00:00.000,000] app: output image, 120x40, 14400 bytes: + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄[...]▄▄| + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄[...]▄▄| + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄[...]▄▄| + ▄▄▄▄▄▄▄▄▄▄▄ shows-up ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄[...]▄▄| + ▄▄▄▄▄▄▄▄▄▄▄ as color ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄[...]▄▄| + ▄▄▄▄▄▄▄▄▄▄▄ on the ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄[...]▄▄| + ▄▄▄▄▄▄▄▄▄▄▄ terminal ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄[...]▄▄| + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄[...]▄▄| + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄[...]▄▄| + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄[...]▄▄| + +.. image:: preview.png diff --git a/samples/lib/pixel/image/preview.png b/samples/lib/pixel/image/preview.png new file mode 100644 index 0000000000000..bb5d4c2034c15 Binary files /dev/null and b/samples/lib/pixel/image/preview.png differ diff --git a/samples/lib/pixel/image/prj.conf b/samples/lib/pixel/image/prj.conf new file mode 100644 index 0000000000000..ee16fb9972b2d --- /dev/null +++ b/samples/lib/pixel/image/prj.conf @@ -0,0 +1,6 @@ +CONFIG_PIXEL=y +CONFIG_ASSERT=y +CONFIG_LOG=y + +# Required to make sure the test harnesses catch the log output +CONFIG_LOG_MODE_IMMEDIATE=y diff --git a/samples/lib/pixel/image/sample.yaml b/samples/lib/pixel/image/sample.yaml new file mode 100644 index 0000000000000..e0641c8459541 --- /dev/null +++ b/samples/lib/pixel/image/sample.yaml @@ -0,0 +1,19 @@ +sample: + description: Pixel image sample, image manipulation library + name: pixel image +common: + min_ram: 32 + tags: pixel + integration_platforms: + - native_sim + harness: console + harness_config: + type: one_line + regex: + - "input image, 32x8, 768 bytes:" + - "output image, 120x40, 14400 bytes:" +tests: + sample.pixel.image: + tags: pixel + extra_configs: + - CONFIG_PIXEL_PRINT_NONE=y diff --git a/samples/lib/pixel/image/src/main.c b/samples/lib/pixel/image/src/main.c new file mode 100644 index 0000000000000..2805959c433e7 --- /dev/null +++ b/samples/lib/pixel/image/src/main.c @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include + +LOG_MODULE_REGISTER(app, LOG_LEVEL_INF); + +static void gradient(uint8_t *rgb24buf, size_t size, const uint8_t beg[3], const uint8_t end[3]) +{ + for (int i = 0; i + 3 <= size; i += 3) { + rgb24buf[i + 0] = (beg[0] * (size - i) + end[0] * i) / size; + rgb24buf[i + 1] = (beg[1] * (size - i) + end[1] * i) / size; + rgb24buf[i + 2] = (beg[2] * (size - i) + end[2] * i) / size; + } +} + +static uint8_t rgb24frame_in[32 * 8 * 3]; +static uint8_t rgb24frame_out[120 * 40 * 3]; + +int main(void) +{ + const uint8_t beg[] = {0x00, 0x70, 0xc5}; + const uint8_t end[] = {0x79, 0x29, 0xd2}; + struct pixel_image img; + + /* Generate a smooth gradient for a small image */ + gradient(rgb24frame_in, sizeof(rgb24frame_in), beg, end); + + /* Open that buffer as an image type */ + pixel_image_from_buffer(&img, rgb24frame_in, sizeof(rgb24frame_in), 32, 8, + VIDEO_PIX_FMT_RGB24); + LOG_INF("input image, %ux%u, %zu bytes:", img.width, img.height, img.size); + pixel_image_print_truecolor(&img); + + /* Turn it into a tall vertical image, now displeasant "banding" artifacts appear */ + pixel_image_resize(&img, 5, 40); + + /* Try to attenuate it with a blur effect (comment this line to see the difference) */ + pixel_image_kernel(&img, PIXEL_KERNEL_GAUSSIAN_BLUR, 5); + + /* Stretch the gradient horizontally over the entire width of the output buffer */ + pixel_image_resize(&img, 120, 40); + + /* Save the image into the output buffer and check for errors */ + pixel_image_to_buffer(&img, rgb24frame_out, sizeof(rgb24frame_out)); + if (img.err != 0) { + LOG_ERR("Image has error %u: %s", img.err, strerror(-img.err)); + return img.err; + } + + /* Now that the imagme is exported, we can print it */ + LOG_INF("output image, %ux%u, %zu bytes:", img.width, img.height, img.size); + pixel_image_print_truecolor(&img); + + return 0; +} diff --git a/samples/lib/pixel/pixel.rst b/samples/lib/pixel/pixel.rst new file mode 100644 index 0000000000000..a202015abf0bf --- /dev/null +++ b/samples/lib/pixel/pixel.rst @@ -0,0 +1,15 @@ +.. zephyr:code-sample-category:: lib_pixel + :name: Pixel Library + :show-listing: + :live-search: + + These samples demonstrate how to use the Pixel processing library of Zephyr. + +These samples can be used as starting point for test benches that print an input image, +perform some custom processing, and print the color image back along with the logs directly +on the terminal. + +This helps debugging when other methods are not available. + +The ``truecolor`` printing functions give accurate 24-bit RGB colors but slower than the +``256color`` variants. diff --git a/samples/lib/pixel/print/CMakeLists.txt b/samples/lib/pixel/print/CMakeLists.txt new file mode 100644 index 0000000000000..1a296153270dc --- /dev/null +++ b/samples/lib/pixel/print/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(pixel) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/lib/pixel/print/README.rst b/samples/lib/pixel/print/README.rst new file mode 100644 index 0000000000000..4056ca0d33f22 --- /dev/null +++ b/samples/lib/pixel/print/README.rst @@ -0,0 +1,57 @@ +.. zephyr:code-sample:: lib_pixel_print + :name: Pixel Printiing Library + + Print images on the console. + +Overview +******** + +A sample showcasing how to make use of the pixel library to visualize an image or histogram data +by printing it out on the console using `ANSI escape codes`_. + +This way debug logs can be interleaved with small preview images for debug purposes. + +.. _ANSI escape codes: https://en.wikipedia.org/wiki/ANSI_escape_code + +Building and Running +******************** + +This application can be built and executed on QEMU as follows: + +.. zephyr-app-commands:: + :zephyr-app: samples/lib/pixel/print + :host-os: unix + :board: native_sim + :goals: run + :compact: + +To build for another board, change "native_sim" above to that board's name. + +Sample Output +============= + +.. code-block:: console + + *** Booting Zephyr OS build v4.1.0-2611-gfaa7b74cfda7 *** + [00:00:00.000,000] app: Printing the gradient #0070c5 -> #7929d2 + [00:00:00.000,000] app: hexdump: + col0 col1 col2 col3 col4 [...] col14 col15 + R G B R G B R G B R G B R G B [...] R G B R G B + 00 70 c5 00 6f c5 00 6f c5 00 6f c5 00 6f c5 [...] 00 6f c5 03 6d c5 row0 + 03 6d c5 04 6d c5 04 6d c5 04 6d c5 04 6d c5 [...] 04 6d c5 07 6b c5 row1 + 07 6b c5 07 6b c5 08 6b c5 08 6b c5 08 6b c5 [...] 07 6b c5 0b 69 c6 row2 + 0b 69 c6 0b 69 c6 0b 69 c6 0c 68 c6 0c 68 c6 [...] 0b 69 c6 0e 67 c6 row3 + 0f 67 c6 0f 66 c6 0f 66 c6 0f 66 c6 10 66 c6 [...] 0f 66 c6 12 65 c7 row4 + 12 64 c7 13 64 c7 13 64 c7 13 64 c7 13 64 c7 [...] 13 64 c7 16 62 c7 row5 + 16 62 c7 16 62 c7 17 62 c7 17 62 c7 17 62 c7 [...] 16 62 c7 1a 60 c7 row6 + [...] + [00:00:00.000,000] app: truecolor: + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| + ▄▄▄ shows-up ▄▄▄| + ▄▄▄ as color ▄▄▄| + ▄▄▄ on the ▄▄▄| + ▄▄▄ terminal ▄▄▄| + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| + [...] + +.. image:: preview.png diff --git a/samples/lib/pixel/print/preview.png b/samples/lib/pixel/print/preview.png new file mode 100644 index 0000000000000..6ac9b19137944 Binary files /dev/null and b/samples/lib/pixel/print/preview.png differ diff --git a/samples/lib/pixel/print/prj.conf b/samples/lib/pixel/print/prj.conf new file mode 100644 index 0000000000000..ee16fb9972b2d --- /dev/null +++ b/samples/lib/pixel/print/prj.conf @@ -0,0 +1,6 @@ +CONFIG_PIXEL=y +CONFIG_ASSERT=y +CONFIG_LOG=y + +# Required to make sure the test harnesses catch the log output +CONFIG_LOG_MODE_IMMEDIATE=y diff --git a/samples/lib/pixel/print/sample.yaml b/samples/lib/pixel/print/sample.yaml new file mode 100644 index 0000000000000..f559a1cd313b4 --- /dev/null +++ b/samples/lib/pixel/print/sample.yaml @@ -0,0 +1,21 @@ +sample: + description: Pixel Print sample, print images in the terminal for debug purpose + name: pixel print +common: + min_ram: 32 + tags: pixel + integration_platforms: + - native_sim + harness: console + harness_config: + type: one_line + regex: + - "truecolor:" + - "256color:" + - "hexdump:" + - "histogram" +tests: + sample.pixel.print: + tags: pixel + extra_configs: + - CONFIG_PIXEL_PRINT_NONE=y diff --git a/samples/lib/pixel/print/src/main.c b/samples/lib/pixel/print/src/main.c new file mode 100644 index 0000000000000..1d7a75bd21b49 --- /dev/null +++ b/samples/lib/pixel/print/src/main.c @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include + +LOG_MODULE_REGISTER(app, LOG_LEVEL_INF); + +static const uint16_t rgb24hist[] = { + 9, 4, 7, 1, 0, 5, 1, 0, 0, 2, 2, 3, 0, 1, 3, 0, + 7, 6, 5, 1, 1, 4, 2, 0, 1, 2, 3, 4, 1, 1, 2, 2, + 8, 4, 7, 4, 2, 3, 1, 2, 2, 2, 2, 2, 0, 0, 1, 1, +}; + +static const uint16_t y8hist[] = { + 8, 5, 6, 2, 1, 4, 1, 1, 1, 2, 3, 3, 1, 1, 2, 1, +}; + +static uint8_t rgb24frame[16 * 32 * 3]; + +void print_image(void) +{ + const uint8_t beg[] = {0x00, 0x70, 0xc5}; + const uint8_t end[] = {0x79, 0x29, 0xd2}; + + /* Generate an image with a gradient of the two colors above */ + for (size_t i = 0, size = sizeof(rgb24frame); i + 3 <= size; i += 3) { + rgb24frame[i + 0] = (beg[0] * (size - i) + end[0] * i) / size; + rgb24frame[i + 1] = (beg[1] * (size - i) + end[1] * i) / size; + rgb24frame[i + 2] = (beg[2] * (size - i) + end[2] * i) / size; + } + + LOG_INF("Printing the gradient #%02x%02x%02x -> #%02x%02x%02x", + beg[0], beg[1], beg[2], end[0], end[1], end[2]); + + LOG_INF("hexdump:"); + pixel_hexdump_rgb24(rgb24frame, sizeof(rgb24frame), 16, 32); + + LOG_INF("truecolor:"); + pixel_print_buffer_truecolor(rgb24frame, sizeof(rgb24frame), 16, 32, VIDEO_PIX_FMT_RGB24); + + LOG_INF("256color:"); + pixel_print_buffer_256color(rgb24frame, sizeof(rgb24frame), 16, 32, VIDEO_PIX_FMT_RGB24); +} + +void print_histogram(void) +{ + LOG_INF("Printing a histogram of %zu RGB buckets", ARRAY_SIZE(rgb24hist)); + pixel_print_rgb24hist(rgb24hist, ARRAY_SIZE(rgb24hist), 8); + + LOG_INF("Printing a histogram of %zu Y (luma) buckets", ARRAY_SIZE(y8hist)); + pixel_print_y8hist(y8hist, ARRAY_SIZE(y8hist), 8); +} + +int main(void) +{ + print_image(); + print_histogram(); + + return 0; +} diff --git a/samples/lib/pixel/stats/CMakeLists.txt b/samples/lib/pixel/stats/CMakeLists.txt new file mode 100644 index 0000000000000..1a296153270dc --- /dev/null +++ b/samples/lib/pixel/stats/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(pixel) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/lib/pixel/stats/README.rst b/samples/lib/pixel/stats/README.rst new file mode 100644 index 0000000000000..c34c04c88ff49 --- /dev/null +++ b/samples/lib/pixel/stats/README.rst @@ -0,0 +1,57 @@ +.. zephyr:code-sample:: lib_pixel_stats + :name: Pixel Statistics Library + + Collect statistics of an image. + +Overview +******** + +A sample showcasing how to make use of the pixel library to collect statistics of an input image +buffer and display both the image and statistics out on the console. + +Building and Running +******************** + +This application can be built and executed on the native simulator as follows: + +.. zephyr-app-commands:: + :zephyr-app: samples/lib/pixel/stats + :host-os: unix + :board: native_sim + :goals: run + :compact: + +To build for another board, change "native_sim" above to that board's name. + +Sample Output +============= + +.. code-block:: console + + *** Booting Zephyr OS build v4.1.0-2611-gfaa7b74cfda7 *** + [00:00:00.000,000] app: Input image preview: + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| + [00:00:00.000,000] app: RGB histogram of the image + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| - 60 + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| - 56 + ▄▄▄▄▄▄▄▄▄▄ shows-up ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| - 52 + ▄▄▄▄▄▄▄▄▄▄ as color ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| - 48 + ▄▄▄▄▄▄▄▄▄▄ on the ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| - 45 + ▄▄▄▄▄▄▄▄▄▄ terminal ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| - 41 + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| - 37 + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| - 33 + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| - 30 + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| - 26 + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| - 22 + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| - 18 + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| - 15 + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| - 11 + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄| - 7 + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + [00:00:00.000,000] app: RGB channel averages of the image + [00:00:00.000,000] app: - R: 0x47/0xff + [00:00:00.000,000] app: - G: 0x88/0xff + [00:00:00.000,000] app: - B: 0xec/0xff + +.. image:: preview.png diff --git a/samples/lib/pixel/stats/preview.png b/samples/lib/pixel/stats/preview.png new file mode 100644 index 0000000000000..7419606ea70fd Binary files /dev/null and b/samples/lib/pixel/stats/preview.png differ diff --git a/samples/lib/pixel/stats/prj.conf b/samples/lib/pixel/stats/prj.conf new file mode 100644 index 0000000000000..ee16fb9972b2d --- /dev/null +++ b/samples/lib/pixel/stats/prj.conf @@ -0,0 +1,6 @@ +CONFIG_PIXEL=y +CONFIG_ASSERT=y +CONFIG_LOG=y + +# Required to make sure the test harnesses catch the log output +CONFIG_LOG_MODE_IMMEDIATE=y diff --git a/samples/lib/pixel/stats/sample.yaml b/samples/lib/pixel/stats/sample.yaml new file mode 100644 index 0000000000000..9784924822736 --- /dev/null +++ b/samples/lib/pixel/stats/sample.yaml @@ -0,0 +1,19 @@ +sample: + description: Pixel Stats sample, collect statistics of an input buffer + name: pixel stats +common: + min_ram: 32 + tags: pixel + integration_platforms: + - native_sim + harness: console + harness_config: + type: one_line + regex: + - "RGB histogram of the image" + - "RGB channel averages of the image" +tests: + sample.pixel.stats: + tags: pixel + extra_configs: + - CONFIG_PIXEL_PRINT_NONE=y diff --git a/samples/lib/pixel/stats/src/main.c b/samples/lib/pixel/stats/src/main.c new file mode 100644 index 0000000000000..47e9fe531f0dd --- /dev/null +++ b/samples/lib/pixel/stats/src/main.c @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(app, LOG_LEVEL_INF); + +#define NVAL 100 + +static const uint8_t image_rgb24[20 * 4 * 3] = { + 0x47, 0x84, 0xee, 0x46, 0x84, 0xee, 0x47, 0x84, 0xee, 0x46, 0x83, 0xee, 0x47, 0x84, 0xee, + 0x78, 0xaa, 0xec, 0x74, 0xb2, 0xe0, 0x67, 0xaa, 0xdd, 0x78, 0xb2, 0xef, 0x39, 0x8c, 0xf1, + 0x3a, 0x8c, 0xf2, 0x39, 0x8b, 0xf1, 0x3a, 0x8b, 0xf1, 0x3a, 0x8b, 0xf1, 0x3a, 0x8b, 0xf1, + 0x3a, 0x8b, 0xf1, 0x3b, 0x8b, 0xf1, 0x3b, 0x8b, 0xf1, 0x3b, 0x8b, 0xf1, 0x3b, 0x8a, 0xf1, + 0x47, 0x82, 0xed, 0x47, 0x82, 0xed, 0x47, 0x82, 0xee, 0x47, 0x82, 0xed, 0x47, 0x82, 0xed, + 0x47, 0x82, 0xed, 0x5d, 0x93, 0xed, 0x5f, 0x9d, 0xeb, 0x3a, 0x8a, 0xf1, 0x3a, 0x8a, 0xf0, + 0x3a, 0x8a, 0xf1, 0x3a, 0x8a, 0xf0, 0x3b, 0x8a, 0xf0, 0x3b, 0x8a, 0xf0, 0x3b, 0x8a, 0xf0, + 0x3b, 0x89, 0xf0, 0x3b, 0x8a, 0xf0, 0x3b, 0x89, 0xf0, 0x3c, 0x89, 0xf0, 0x3c, 0x89, 0xf0, + 0x49, 0x82, 0xee, 0x49, 0x82, 0xed, 0x49, 0x82, 0xee, 0x48, 0x82, 0xed, 0x49, 0x82, 0xee, + 0x49, 0x82, 0xed, 0x73, 0x92, 0xe9, 0x50, 0x65, 0xd4, 0x4c, 0x93, 0xf2, 0x3c, 0x8a, 0xf1, + 0x3c, 0x8a, 0xf1, 0x3c, 0x8a, 0xf0, 0x3c, 0x8a, 0xf1, 0x3c, 0x89, 0xf0, 0x3d, 0x8a, 0xf1, + 0x3d, 0x89, 0xf0, 0x3d, 0x89, 0xf0, 0x3d, 0x89, 0xf0, 0x3e, 0x89, 0xf0, 0x3d, 0x89, 0xf0, + 0x4a, 0x81, 0xed, 0x49, 0x81, 0xed, 0x4a, 0x81, 0xed, 0x49, 0x81, 0xed, 0x49, 0x81, 0xed, + 0x71, 0x8c, 0xe5, 0x3e, 0x4c, 0xcc, 0x3d, 0x4c, 0xcb, 0x65, 0x85, 0xe1, 0x3d, 0x89, 0xf0, + 0x3d, 0x89, 0xf0, 0x3d, 0x88, 0xf0, 0x3d, 0x89, 0xf0, 0x3d, 0x88, 0xf0, 0x3e, 0x88, 0xf0, + 0x3e, 0x88, 0xf0, 0x3e, 0x88, 0xf0, 0x3e, 0x88, 0xf0, 0x3f, 0x88, 0xf0, 0x3e, 0x88, 0xef, +}; + +static uint16_t rgb24hist[3 * 64]; +static uint8_t rgb24avg[3]; + +int main(void) +{ + LOG_INF("Input image preview:"); + pixel_print_buffer_truecolor(image_rgb24, sizeof(image_rgb24), 20, 4, VIDEO_PIX_FMT_RGB24); + + LOG_INF("RGB histogram of the image"); + pixel_rgb24frame_to_rgb24hist(image_rgb24, sizeof(image_rgb24), + rgb24hist, ARRAY_SIZE(rgb24hist), NVAL); + pixel_print_rgb24hist(rgb24hist, ARRAY_SIZE(rgb24hist), 16); + + LOG_INF("RGB channel averages of the image"); + pixel_rgb24frame_to_rgb24avg(image_rgb24, sizeof(image_rgb24), rgb24avg, NVAL); + LOG_INF("- R: 0x%02x/0xff", rgb24avg[0]); + LOG_INF("- G: 0x%02x/0xff", rgb24avg[1]); + LOG_INF("- B: 0x%02x/0xff", rgb24avg[2]); + + return 0; +} diff --git a/tests/lib/pixel/bayer/CMakeLists.txt b/tests/lib/pixel/bayer/CMakeLists.txt new file mode 100644 index 0000000000000..8780ed12afb96 --- /dev/null +++ b/tests/lib/pixel/bayer/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(lib_pixel_convert) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/lib/pixel/bayer/prj.conf b/tests/lib/pixel/bayer/prj.conf new file mode 100644 index 0000000000000..c5261e0c19f60 --- /dev/null +++ b/tests/lib/pixel/bayer/prj.conf @@ -0,0 +1,4 @@ +CONFIG_ASSERT=y +CONFIG_PIXEL_LOG_LEVEL_DBG=y +CONFIG_ZTEST=y +CONFIG_PIXEL=y diff --git a/tests/lib/pixel/bayer/src/main.c b/tests/lib/pixel/bayer/src/main.c new file mode 100644 index 0000000000000..bf106e02ed363 --- /dev/null +++ b/tests/lib/pixel/bayer/src/main.c @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include + +#define WIDTH 16 +#define HEIGHT 16 + +#define ERROR_MARGIN 13 + +static uint8_t bayerframe_in[WIDTH * HEIGHT * 1]; +static uint8_t rgb24frame_out[WIDTH * HEIGHT * 3]; + +void test_bayer(uint32_t fourcc, uint32_t window_size, uint32_t expected_color) +{ + uint8_t r = expected_color >> 16; + uint8_t g = expected_color >> 8; + uint8_t b = expected_color >> 0; + struct pixel_image img; + int ret; + + pixel_image_from_buffer(&img, bayerframe_in, sizeof(bayerframe_in), WIDTH, HEIGHT, fourcc); + + printf("input:\n"); + pixel_image_print_truecolor(&img); + + ret = pixel_image_debayer(&img, window_size); + zassert_ok(ret); + + pixel_image_to_buffer(&img, rgb24frame_out, sizeof(rgb24frame_out)); + + printf("output: (expecting #%06x, R:%02x G:%02x B:%02x)\n", expected_color, r, g, b); + pixel_image_print_truecolor(&img); + + for (int i = 0; i < sizeof(rgb24frame_out) / 3; i++) { + uint8_t out_r = rgb24frame_out[i * 3 + 0]; + uint8_t out_g = rgb24frame_out[i * 3 + 1]; + uint8_t out_b = rgb24frame_out[i * 3 + 2]; + char *s = VIDEO_FOURCC_TO_STR(fourcc); + + zassert_equal(r, out_r, "R: %s: expected 0x%02x, obtained 0x%02x", s, r, out_r); + zassert_equal(g, out_g, "G: %s: expected 0x%02x, obtained 0x%02x", s, g, out_g); + zassert_equal(b, out_b, "B: %s: expected 0x%02x, obtained 0x%02x", s, b, out_b); + } +} + +ZTEST(lib_pixel_bayer, test_pixel_bayer_operation) +{ + /* Generate test input data for 2x2 debayer */ + for (size_t h = 0; h < HEIGHT; h++) { + memset(bayerframe_in + h * WIDTH, h % 2 ? 0xff : 0x00, WIDTH * 1); + } + + test_bayer(VIDEO_PIX_FMT_RGGB8, 2, 0x007fff); + test_bayer(VIDEO_PIX_FMT_GRBG8, 2, 0x007fff); + test_bayer(VIDEO_PIX_FMT_BGGR8, 2, 0xff7f00); + test_bayer(VIDEO_PIX_FMT_GBRG8, 2, 0xff7f00); + + /* Generate test input data for 3x3 debayer */ + for (size_t h = 0; h < HEIGHT; h++) { + for (size_t w = 0; w < WIDTH; w++) { + bayerframe_in[h * WIDTH + w] = (h + w) % 2 ? 0xff : 0x00; + } + } + + test_bayer(VIDEO_PIX_FMT_RGGB8, 3, 0x00ff00); + test_bayer(VIDEO_PIX_FMT_GBRG8, 3, 0xff00ff); + test_bayer(VIDEO_PIX_FMT_BGGR8, 3, 0x00ff00); + test_bayer(VIDEO_PIX_FMT_GRBG8, 3, 0xff00ff); +} + +ZTEST_SUITE(lib_pixel_bayer, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/lib/pixel/bayer/testcase.yaml b/tests/lib/pixel/bayer/testcase.yaml new file mode 100644 index 0000000000000..8e046a4aeadc2 --- /dev/null +++ b/tests/lib/pixel/bayer/testcase.yaml @@ -0,0 +1,9 @@ +tests: + libraries.pixel.bayer: + tags: + - pixel + integration_platforms: + - qemu_cortex_m3 + - native_sim + extra_configs: + - CONFIG_PIXEL_PRINT_NONE=y diff --git a/tests/lib/pixel/convert/CMakeLists.txt b/tests/lib/pixel/convert/CMakeLists.txt new file mode 100644 index 0000000000000..8780ed12afb96 --- /dev/null +++ b/tests/lib/pixel/convert/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(lib_pixel_convert) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/lib/pixel/convert/prj.conf b/tests/lib/pixel/convert/prj.conf new file mode 100644 index 0000000000000..c5261e0c19f60 --- /dev/null +++ b/tests/lib/pixel/convert/prj.conf @@ -0,0 +1,4 @@ +CONFIG_ASSERT=y +CONFIG_PIXEL_LOG_LEVEL_DBG=y +CONFIG_ZTEST=y +CONFIG_PIXEL=y diff --git a/tests/lib/pixel/convert/src/main.c b/tests/lib/pixel/convert/src/main.c new file mode 100644 index 0000000000000..d3d673bf36937 --- /dev/null +++ b/tests/lib/pixel/convert/src/main.c @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include +#include + +#define WIDTH 16 +#define HEIGHT 16 + +#define ERROR_MARGIN 13 + +/* + * To get YUV BT.709 test data: + * + * ffmpeg -y -f lavfi -colorspace bt709 -i color=#RRGGBB:2x2:d=3,format=rgb24 \ + * -f rawvideo -pix_fmt yuyv422 - | hexdump -C + * + * To get RGB565 test data: + * + * ffmpeg -y -f lavfi -i color=#RRGGBB:2x2:d=3,format=rgb24 \ + * -f rawvideo -pix_fmt rgb565 - | hexdump -C + */ + +const struct color_ref { + uint8_t rgb24[3]; + uint8_t rgb565[2]; + uint8_t rgb332[1]; + uint8_t yuv24_bt709[3]; + uint8_t yuv24_bt601[3]; +} reference_data[] = { + + /* Primary colors */ + {{0x00, 0x00, 0x00}, {0x00, 0x00}, {0x00}, {0x10, 0x80, 0x80}, {0x10, 0x80, 0x80}}, + {{0x00, 0x00, 0xff}, {0x00, 0x1f}, {0x03}, {0x20, 0xf0, 0x76}, {0x29, 0xf1, 0x6e}}, + {{0x00, 0xff, 0x00}, {0x07, 0xe0}, {0x1c}, {0xad, 0x2a, 0x1a}, {0x9a, 0x2a, 0x35}}, + {{0x00, 0xff, 0xff}, {0x07, 0xff}, {0x1f}, {0xbc, 0x9a, 0x10}, {0xb4, 0xa0, 0x23}}, + {{0xff, 0x00, 0x00}, {0xf8, 0x00}, {0xe0}, {0x3f, 0x66, 0xf0}, {0x50, 0x5b, 0xee}}, + {{0xff, 0x00, 0xff}, {0xf8, 0x1f}, {0xe3}, {0x4e, 0xd6, 0xe6}, {0x69, 0xcb, 0xdc}}, + {{0xff, 0xff, 0x00}, {0xff, 0xe0}, {0xfc}, {0xdb, 0x10, 0x8a}, {0xd0, 0x0a, 0x93}}, + {{0xff, 0xff, 0xff}, {0xff, 0xff}, {0xff}, {0xeb, 0x80, 0x80}, {0xeb, 0x80, 0x80}}, + + /* Arbitrary colors */ + {{0x00, 0x70, 0xc5}, {0x03, 0x98}, {0x0f}, {0x61, 0xb1, 0x4b}, {0x5e, 0xb5, 0x4d}}, + {{0x33, 0x8d, 0xd1}, {0x3c, 0x7a}, {0x33}, {0x7d, 0xa7, 0x56}, {0x7b, 0xab, 0x57}}, + {{0x66, 0xa9, 0xdc}, {0x6d, 0x5b}, {0x77}, {0x98, 0x9d, 0x61}, {0x96, 0xa0, 0x61}}, + {{0x7d, 0xd2, 0xf7}, {0x86, 0x9e}, {0x7b}, {0xb7, 0x99, 0x59}, {0xb3, 0x9d, 0x5a}}, + {{0x97, 0xdb, 0xf9}, {0x9e, 0xde}, {0x9b}, {0xc2, 0x94, 0x61}, {0xbf, 0x97, 0x62}}, + {{0xb1, 0xe4, 0xfa}, {0xb7, 0x3f}, {0xbf}, {0xcc, 0x8f, 0x69}, {0xca, 0x91, 0x69}}, + {{0x79, 0x29, 0xd2}, {0x79, 0x5a}, {0x67}, {0x4c, 0xc2, 0x9c}, {0x57, 0xbf, 0x96}}, + {{0x94, 0x54, 0xdb}, {0x9a, 0xbb}, {0x8b}, {0x6c, 0xb5, 0x97}, {0x75, 0xb3, 0x92}}, + {{0xaf, 0x7f, 0xe4}, {0xb3, 0xfc}, {0xaf}, {0x8c, 0xa8, 0x91}, {0x93, 0xa6, 0x8d}}, +}; + +static uint8_t line_in[WIDTH * 4]; +static uint8_t line_out[WIDTH * 4]; + +void test_conversion(const uint8_t *pix_in, uint32_t fourcc_in, size_t pix_in_step, + const uint8_t *pix_out, uint32_t fourcc_out, size_t pix_out_step, + void (*fn)(const uint8_t *in, uint8_t *out, uint16_t width)) +{ + size_t pix_in_size = video_bits_per_pixel(fourcc_in) / BITS_PER_BYTE; + size_t pix_out_size = video_bits_per_pixel(fourcc_out) / BITS_PER_BYTE; + bool done = false; + + /* Fill the input line as much as possible */ + for (size_t w = 0; w < WIDTH; w += pix_in_step) { + memcpy(&line_in[w * pix_in_size], pix_in, pix_in_size * pix_in_step); + } + + /* Perform the conversion to test */ + fn(line_in, line_out, WIDTH); + + printf("\n"); + + printf("out:"); + for (int i = 0; i < pix_out_step * pix_out_size; i++) { + printf(" %02x", line_out[i]); + } + printf(" |"); + pixel_print_buffer_truecolor(line_out, sizeof(line_out), WIDTH / 2, 2, fourcc_out); + + printf("ref:"); + for (int i = 0; i < pix_out_step * pix_out_size; i++) { + printf(" %02x", pix_out[i]); + } + printf(" |"); + pixel_print_buffer_truecolor(line_in, sizeof(line_in), WIDTH / 2, 2, fourcc_in); + + /* Scan the result against the reference output pixel to make sure it worked */ + for (size_t w = 0; w < WIDTH; w += pix_out_step) { + for (int i = 0; w * pix_out_size + i < (w + pix_out_step) * pix_out_size; i++) { + zassert_within(line_out[w * pix_out_size + i], pix_out[i], 9, + "at %u: value 0x%02x, reference 0x%02x", + i, line_out[w * pix_out_size + i], pix_out[i]); + } + + /* Make sure we visited that loop */ + done = true; + } + + zassert_true(done); +} + +ZTEST(lib_pixel_convert, test_pixel_convert_line) +{ + for (size_t i = 0; i < ARRAY_SIZE(reference_data); i++) { + /* The current color we are testing */ + const struct color_ref *ref = &reference_data[i]; + + /* Generate very small buffers out of the reference tables */ + const uint8_t rgb24[] = { + ref->rgb24[0], + ref->rgb24[1], + ref->rgb24[2], + }; + const uint8_t rgb565be[] = { + ref->rgb565[0], + ref->rgb565[1], + }; + const uint8_t rgb565le[] = { + ref->rgb565[1], + ref->rgb565[0], + }; + const uint8_t rgb332[] = { + ref->rgb332[0], + }; + const uint8_t yuv24_bt709[] = { + ref->yuv24_bt709[0], + ref->yuv24_bt709[1], + ref->yuv24_bt709[2], + }; + const uint8_t yuyv_bt709[] = { + ref->yuv24_bt709[0], + ref->yuv24_bt709[1], + ref->yuv24_bt709[0], + ref->yuv24_bt709[2], + }; + + printf("\nColor #%02x%02x%02x\n", ref->rgb24[0], ref->rgb24[1], ref->rgb24[2]); + + test_conversion(rgb24, VIDEO_PIX_FMT_RGB24, 1, rgb565be, + VIDEO_PIX_FMT_RGB565X, 1, &pixel_line_rgb24_to_rgb565be); + test_conversion(rgb24, VIDEO_PIX_FMT_RGB24, 1, rgb565le, + VIDEO_PIX_FMT_RGB565, 1, &pixel_line_rgb24_to_rgb565le); + test_conversion(rgb24, VIDEO_PIX_FMT_RGB24, 1, rgb332, + VIDEO_PIX_FMT_RGB332, 1, &pixel_line_rgb24_to_rgb332); + test_conversion(rgb565be, VIDEO_PIX_FMT_RGB565X, 1, rgb24, + VIDEO_PIX_FMT_RGB24, 1, &pixel_line_rgb565be_to_rgb24); + test_conversion(rgb565le, VIDEO_PIX_FMT_RGB565, 1, rgb24, + VIDEO_PIX_FMT_RGB24, 1, &pixel_line_rgb565le_to_rgb24); + test_conversion(rgb24, VIDEO_PIX_FMT_RGB24, 1, yuyv_bt709, + VIDEO_PIX_FMT_YUYV, 2, &pixel_line_rgb24_to_yuyv_bt709); + test_conversion(yuyv_bt709, VIDEO_PIX_FMT_YUYV, 2, rgb24, + VIDEO_PIX_FMT_RGB24, 1, &pixel_line_yuyv_to_rgb24_bt709); + test_conversion(rgb24, VIDEO_PIX_FMT_RGB24, 1, yuv24_bt709, + VIDEO_PIX_FMT_YUV24, 1, &pixel_line_rgb24_to_yuv24_bt709); + test_conversion(yuv24_bt709, VIDEO_PIX_FMT_YUV24, 1, rgb24, + VIDEO_PIX_FMT_RGB24, 1, &pixel_line_yuv24_to_rgb24_bt709); + test_conversion(yuv24_bt709, VIDEO_PIX_FMT_YUV24, 1, yuyv_bt709, + VIDEO_PIX_FMT_YUYV, 2, &pixel_line_yuv24_to_yuyv); + test_conversion(yuyv_bt709, VIDEO_PIX_FMT_YUYV, 2, yuv24_bt709, + VIDEO_PIX_FMT_YUV24, 1, &pixel_line_yuyv_to_yuv24); + } +} + +static uint8_t rgb24frame_in[WIDTH * HEIGHT * 3]; +static uint8_t rgb24frame_out[WIDTH * HEIGHT * 3]; + +ZTEST(lib_pixel_convert, test_pixel_convert_operation) +{ + struct pixel_image img; + int ret; + + /* Generate test input data */ + for (size_t i = 0; i < sizeof(rgb24frame_in); i++) { + rgb24frame_in[i] = i / 3; + } + + pixel_image_from_buffer(&img, rgb24frame_in, sizeof(rgb24frame_in), + WIDTH, HEIGHT, VIDEO_PIX_FMT_RGB24); + + printf("input:\n"); + pixel_image_print_truecolor(&img); + + ret = pixel_image_convert(&img, VIDEO_PIX_FMT_RGB24); + zassert_ok(ret); + + /* Test the RGB24 <-> RGB565 conversion */ + ret = pixel_image_convert(&img, VIDEO_PIX_FMT_RGB565); + zassert_ok(ret); + ret = pixel_image_convert(&img, VIDEO_PIX_FMT_RGB24); + zassert_ok(ret); + + /* Test the RGB24 <-> RGB565X conversion */ + ret = pixel_image_convert(&img, VIDEO_PIX_FMT_RGB565X); + zassert_ok(ret); + ret = pixel_image_convert(&img, VIDEO_PIX_FMT_RGB24); + zassert_ok(ret); + + /* Test the RGB24 <-> YUV24 conversion */ + ret = pixel_image_convert(&img, VIDEO_PIX_FMT_YUV24); + zassert_ok(ret); + ret = pixel_image_convert(&img, VIDEO_PIX_FMT_RGB24); + zassert_ok(ret); + + /* Test the YUYV <-> YUV24 conversion */ + ret = pixel_image_convert(&img, VIDEO_PIX_FMT_YUYV); + zassert_ok(ret); + ret = pixel_image_convert(&img, VIDEO_PIX_FMT_YUV24); + zassert_ok(ret); + ret = pixel_image_convert(&img, VIDEO_PIX_FMT_YUYV); + zassert_ok(ret); + ret = pixel_image_convert(&img, VIDEO_PIX_FMT_RGB24); + zassert_ok(ret); + + pixel_image_to_buffer(&img, rgb24frame_out, sizeof(rgb24frame_out)); + + printf("output:\n"); + pixel_image_print_truecolor(&img); + + for (int i = 0; i < sizeof(rgb24frame_out); i++) { + /* Precision is not 100% as some conversions steps are lossy */ + zassert_within(rgb24frame_in[i], rgb24frame_out[i], ERROR_MARGIN, + "Testing position %u", i); + } +} + +ZTEST_SUITE(lib_pixel_convert, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/lib/pixel/convert/testcase.yaml b/tests/lib/pixel/convert/testcase.yaml new file mode 100644 index 0000000000000..9377cc1fa5689 --- /dev/null +++ b/tests/lib/pixel/convert/testcase.yaml @@ -0,0 +1,9 @@ +tests: + libraries.pixel.convert: + tags: + - pixel + integration_platforms: + - qemu_cortex_m3 + - native_sim + extra_configs: + - CONFIG_PIXEL_PRINT_NONE=y diff --git a/tests/lib/pixel/kernel/CMakeLists.txt b/tests/lib/pixel/kernel/CMakeLists.txt new file mode 100644 index 0000000000000..a7fc55f4a48e5 --- /dev/null +++ b/tests/lib/pixel/kernel/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(lib_pixel_kernel) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/lib/pixel/kernel/prj.conf b/tests/lib/pixel/kernel/prj.conf new file mode 100644 index 0000000000000..c5261e0c19f60 --- /dev/null +++ b/tests/lib/pixel/kernel/prj.conf @@ -0,0 +1,4 @@ +CONFIG_ASSERT=y +CONFIG_PIXEL_LOG_LEVEL_DBG=y +CONFIG_ZTEST=y +CONFIG_PIXEL=y diff --git a/tests/lib/pixel/kernel/src/main.c b/tests/lib/pixel/kernel/src/main.c new file mode 100644 index 0000000000000..5c7e451680ea4 --- /dev/null +++ b/tests/lib/pixel/kernel/src/main.c @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include + +#define WIDTH 20 +#define HEIGHT 20 + +/* Input/output buffers */ +static uint8_t rgb24frame_in[WIDTH * HEIGHT * 3]; +static uint8_t rgb24frame_out[WIDTH * HEIGHT * 3]; + +static void run_kernel(uint32_t kernel_type, uint32_t kernel_size) +{ + struct pixel_image img; + int ret; + + pixel_image_from_buffer(&img, rgb24frame_in, sizeof(rgb24frame_in), WIDTH, HEIGHT, + VIDEO_PIX_FMT_RGB24); + + printf("input:\n"); + pixel_image_print_truecolor(&img); + + ret = pixel_image_kernel(&img, kernel_type, kernel_size); + zassert_ok(ret); + + ret = pixel_image_to_buffer(&img, rgb24frame_out, sizeof(rgb24frame_out)); + zassert_ok(ret); + + printf("output:\n"); + pixel_image_print_truecolor(&img); +} + +static void test_identity(uint32_t kernel_size) +{ + run_kernel(PIXEL_KERNEL_IDENTITY, kernel_size); + + for (uint16_t h = 0; h < HEIGHT; h++) { + for (uint16_t w = 0; w < WIDTH; w++) { + size_t i = h * WIDTH * 3 + w * 3; + + zassert_equal(rgb24frame_out[i + 0], rgb24frame_in[i + 0], + "channel R, row %u, col %u", h, w); + zassert_equal(rgb24frame_out[i + 1], rgb24frame_in[i + 1], + "channel G, row %u, col %u", h, w); + zassert_equal(rgb24frame_out[i + 2], rgb24frame_in[i + 2], + "channel B, row %u, col %u", h, w); + } + } +} + +static void test_median(uint32_t kernel_size) +{ + run_kernel(PIXEL_KERNEL_DENOISE, kernel_size); + + for (uint16_t h = 0; h < HEIGHT; h++) { + uint16_t w = 0; + + /* Left half */ + for (; w < WIDTH / 2 - 1; w++) { + size_t i = h * WIDTH * 3 + w * 3; + + zassert_equal(rgb24frame_out[i + 0], rgb24frame_out[i + 3], + "channel R, row %u, col %u", h, w); + zassert_equal(rgb24frame_out[i + 1], rgb24frame_out[i + 4], + "channel G, row %u, col %u", h, w); + zassert_equal(rgb24frame_out[i + 2], rgb24frame_out[i + 5], + "channel B, row %u, col %u", h, w); + } + + /* Left right */ + for (; w < WIDTH / 2 - 1; w++) { + size_t i = h * WIDTH * 3 + w * 3; + + zassert_equal(rgb24frame_out[i + 0], rgb24frame_out[i + 3], + "channel R, row %u, col %u", h, w); + zassert_equal(rgb24frame_out[i + 1], rgb24frame_out[i + 4], + "channel G, row %u, col %u", h, w); + zassert_equal(rgb24frame_out[i + 2], rgb24frame_out[i + 5], + "channel B, row %u, col %u", h, w); + } + } +} + +static void test_blur(uint32_t kernel_size, int blur_margin) +{ + run_kernel(PIXEL_KERNEL_GAUSSIAN_BLUR, kernel_size); + + for (uint16_t h = 0; h < HEIGHT; h++) { + uint16_t w = 0; + + for (; w < WIDTH - 1; w++) { + size_t i = h * WIDTH * 3 + w * 3; + + zassert_within(rgb24frame_out[i + 0], rgb24frame_out[i + 3], blur_margin, + "channel R, row %u, col %u", h, w); + zassert_within(rgb24frame_out[i + 1], rgb24frame_out[i + 4], blur_margin, + "channel G, row %u, col %u", h, w); + zassert_within(rgb24frame_out[i + 2], rgb24frame_out[i + 5], blur_margin, + "channel B, row %u, col %u", h, w); + } + } +} + +ZTEST(lib_pixel_kernel, test_pixel_identity_kernel) +{ + /* Generate test input data */ + for (uint16_t h = 0; h < HEIGHT; h++) { + for (uint16_t w = 0; w < WIDTH; w++) { + rgb24frame_in[h * WIDTH * 3 + w * 3 + 0] = w < WIDTH / 2 ? 0x00 : 0xff; + rgb24frame_in[h * WIDTH * 3 + w * 3 + 1] = (h % 3 + w % 3) / 4 * 0xff; + rgb24frame_in[h * WIDTH * 3 + w * 3 + 2] = h * 0xff / HEIGHT; + } + } + + test_identity(3); + test_identity(5); + + test_median(3); + test_median(5); + + test_blur(3, 128); + test_blur(5, 96); +} + +ZTEST_SUITE(lib_pixel_kernel, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/lib/pixel/kernel/testcase.yaml b/tests/lib/pixel/kernel/testcase.yaml new file mode 100644 index 0000000000000..c307fe1f5c4b3 --- /dev/null +++ b/tests/lib/pixel/kernel/testcase.yaml @@ -0,0 +1,9 @@ +tests: + libraries.pixel.kernel: + tags: + - pixel + integration_platforms: + - qemu_cortex_m3 + - native_sim + extra_configs: + - CONFIG_PIXEL_PRINT_NONE=y diff --git a/tests/lib/pixel/resize/CMakeLists.txt b/tests/lib/pixel/resize/CMakeLists.txt new file mode 100644 index 0000000000000..463b06b146d36 --- /dev/null +++ b/tests/lib/pixel/resize/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(lib_pixel_resize) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/lib/pixel/resize/prj.conf b/tests/lib/pixel/resize/prj.conf new file mode 100644 index 0000000000000..c5261e0c19f60 --- /dev/null +++ b/tests/lib/pixel/resize/prj.conf @@ -0,0 +1,4 @@ +CONFIG_ASSERT=y +CONFIG_PIXEL_LOG_LEVEL_DBG=y +CONFIG_ZTEST=y +CONFIG_PIXEL=y diff --git a/tests/lib/pixel/resize/src/main.c b/tests/lib/pixel/resize/src/main.c new file mode 100644 index 0000000000000..3af81a12fd85d --- /dev/null +++ b/tests/lib/pixel/resize/src/main.c @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2025 tinyVision.ai Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include + +#define WIDTH_IN 6 +#define HEIGHT_IN 10 +#define PITCH_IN (WIDTH_IN * 3) + +#define WIDTH_OUT 4 +#define HEIGHT_OUT 22 +#define PITCH_OUT (WIDTH_OUT * 3) + +#define ERROR_MARGIN 9 + +/* Input/output buffers */ +static uint8_t rgb24frame_in[WIDTH_IN * HEIGHT_IN * 3]; +static uint8_t rgb24frame_out[WIDTH_OUT * HEIGHT_OUT * 3]; + +static void test_resize(uint32_t fourcc) +{ + struct pixel_image img; + size_t w = WIDTH_OUT; + size_t h = HEIGHT_OUT; + size_t p = PITCH_OUT; + int ret; + + pixel_image_from_buffer(&img, rgb24frame_in, sizeof(rgb24frame_in), WIDTH_IN, HEIGHT_IN, + VIDEO_PIX_FMT_RGB24); + + printf("input:\n"); + pixel_image_print_truecolor(&img); + + ret = pixel_image_convert(&img, fourcc); + zassert_ok(ret); + ret = pixel_image_resize(&img, WIDTH_OUT, HEIGHT_OUT); + zassert_ok(ret); + ret = pixel_image_convert(&img, VIDEO_PIX_FMT_RGB24); + zassert_ok(ret); + ret = pixel_image_to_buffer(&img, rgb24frame_out, sizeof(rgb24frame_out)); + zassert_ok(ret); + + printf("output:\n"); + pixel_image_print_truecolor(&img); + + /* Test top left quadramt */ + zassert_within(rgb24frame_out[(0) * p + (0) * 3 + 0], 0x00, ERROR_MARGIN); + zassert_within(rgb24frame_out[(0) * p + (0) * 3 + 1], 0x00, ERROR_MARGIN); + zassert_within(rgb24frame_out[(0) * p + (0) * 3 + 2], 0x7f, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h / 2 - 1) * p + (w / 2 - 1) * 3 + 0], 0x00, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h / 2 - 1) * p + (w / 2 - 1) * 3 + 1], 0x00, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h / 2 - 1) * p + (w / 2 - 1) * 3 + 2], 0x7f, ERROR_MARGIN); + + /* Test bottom left quadrant */ + zassert_within(rgb24frame_out[(h - 1) * p + (0) * 3 + 0], 0x00, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h - 1) * p + (0) * 3 + 1], 0xff, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h - 1) * p + (0) * 3 + 2], 0x7f, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h / 2 + 1) * p + (w / 2 - 1) * 3 + 0], 0x00, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h / 2 + 1) * p + (w / 2 - 1) * 3 + 1], 0xff, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h / 2 + 1) * p + (w / 2 - 1) * 3 + 2], 0x7f, ERROR_MARGIN); + + /* Test top right quadrant */ + zassert_within(rgb24frame_out[(0) * p + (w - 1) * 3 + 0], 0xff, ERROR_MARGIN); + zassert_within(rgb24frame_out[(0) * p + (w - 1) * 3 + 1], 0x00, ERROR_MARGIN); + zassert_within(rgb24frame_out[(0) * p + (w - 1) * 3 + 2], 0x7f, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h / 2 - 1) * p + (w / 2 + 1) * 3 + 0], 0xff, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h / 2 - 1) * p + (w / 2 + 1) * 3 + 1], 0x00, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h / 2 - 1) * p + (w / 2 + 1) * 3 + 2], 0x7f, ERROR_MARGIN); + + /* Test bottom right quadrant */ + zassert_within(rgb24frame_out[(h - 1) * p + (w - 1) * 3 + 0], 0xff, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h - 1) * p + (w - 1) * 3 + 1], 0xff, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h - 1) * p + (w - 1) * 3 + 2], 0x7f, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h / 2 + 1) * p + (w / 2 + 1) * 3 + 0], 0xff, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h / 2 + 1) * p + (w / 2 + 1) * 3 + 1], 0xff, ERROR_MARGIN); + zassert_within(rgb24frame_out[(h / 2 + 1) * p + (w / 2 + 1) * 3 + 2], 0x7f, ERROR_MARGIN); +} + +ZTEST(lib_pixel_resize, test_pixel_resize_operation) +{ + /* Generate test input data */ + for (uint16_t h = 0; h < HEIGHT_IN; h++) { + for (uint16_t w = 0; w < WIDTH_IN; w++) { + rgb24frame_in[h * PITCH_IN + w * 3 + 0] = w < WIDTH_IN / 2 ? 0x00 : 0xff; + rgb24frame_in[h * PITCH_IN + w * 3 + 1] = h < HEIGHT_IN / 2 ? 0x00 : 0xff; + rgb24frame_in[h * PITCH_IN + w * 3 + 2] = 0x7f; + } + } + + test_resize(VIDEO_PIX_FMT_RGB24); + test_resize(VIDEO_PIX_FMT_RGB565); + test_resize(VIDEO_PIX_FMT_RGB565X); +} + +ZTEST_SUITE(lib_pixel_resize, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/lib/pixel/resize/testcase.yaml b/tests/lib/pixel/resize/testcase.yaml new file mode 100644 index 0000000000000..a51a5a197ae68 --- /dev/null +++ b/tests/lib/pixel/resize/testcase.yaml @@ -0,0 +1,9 @@ +tests: + libraries.pixel.resize: + tags: + - pixel + integration_platforms: + - qemu_cortex_m3 + - native_sim + extra_configs: + - CONFIG_PIXEL_PRINT_NONE=y