diff --git a/CHANGELOG.md b/CHANGELOG.md index 28d2f58e8ec068c222e930c40f7f2289a58e2d68..6dc6739e42d68694e82c5e8f5e7b1bfc13527378 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ This changelog aims to follow the guiding principles of ## 0.5.0 - not yet released +### Added +- Median Blur for 5x5 kernels. + ## 0.4.0 - 2025-03-25 ### Added diff --git a/adapters/opencv/kleidicv_hal.cpp b/adapters/opencv/kleidicv_hal.cpp index 7a554af6cd143ea31d7745dc68d603a38e971761..a46b89d10fef8d45c98a1ff6a7c76cb83eafb40e 100644 --- a/adapters/opencv/kleidicv_hal.cpp +++ b/adapters/opencv/kleidicv_hal.cpp @@ -952,6 +952,38 @@ int sobel(const uchar *src_data, size_t src_step, uchar *dst_data, return CV_HAL_ERROR_NOT_IMPLEMENTED; } +int medianBlur(const uchar *src_data, size_t src_step, uchar *dst_data, + size_t dst_step, int width, int height, int depth, int cn, + int ksize) { + auto mt = get_multithreading(); + if (depth == CV_8U) { + return convert_error(kleidicv_thread_median_blur_u8( + reinterpret_cast(src_data), src_step, + reinterpret_cast(dst_data), dst_step, width, height, cn, + ksize, ksize, kleidicv_border_type_t::KLEIDICV_BORDER_TYPE_REPLICATE, + mt)); + } else if (depth == CV_16S) { + return convert_error(kleidicv_thread_median_blur_s16( + reinterpret_cast(src_data), src_step, + reinterpret_cast(dst_data), dst_step, width, height, cn, + ksize, ksize, kleidicv_border_type_t::KLEIDICV_BORDER_TYPE_REPLICATE, + mt)); + } else if (depth == CV_16U) { + return convert_error(kleidicv_thread_median_blur_u16( + reinterpret_cast(src_data), src_step, + reinterpret_cast(dst_data), dst_step, width, height, cn, + ksize, ksize, kleidicv_border_type_t::KLEIDICV_BORDER_TYPE_REPLICATE, + mt)); + } else if (depth == CV_32F) { + return convert_error(kleidicv_thread_median_blur_f32( + reinterpret_cast(src_data), src_step, + reinterpret_cast(dst_data), dst_step, width, height, cn, ksize, + ksize, kleidicv_border_type_t::KLEIDICV_BORDER_TYPE_REPLICATE, mt)); + } else { + return CV_HAL_ERROR_NOT_IMPLEMENTED; + } +} + #if KLEIDICV_EXPERIMENTAL_FEATURE_CANNY int canny(const uchar *src_data, size_t src_step, uchar *dst_data, size_t dst_step, int width, int height, int cn, double lowThreshold, diff --git a/adapters/opencv/kleidicv_hal.h b/adapters/opencv/kleidicv_hal.h index f10f21c2c46c1d994461e39e9300fd14f8066705..d83013ce9f962696fca3dfef780ab05660488619 100644 --- a/adapters/opencv/kleidicv_hal.h +++ b/adapters/opencv/kleidicv_hal.h @@ -105,6 +105,10 @@ int sobel(const uchar *src_data, size_t src_step, uchar *dst_data, int margin_bottom, int dx, int dy, int ksize, double scale, double delta, int border_type); +int medianBlur(const uchar *src_data, size_t src_step, uchar *dst_data, + size_t dst_step, int width, int height, int depth, int cn, + int ksize); + int compare_u8(const uchar *src1_data, size_t src1_step, const uchar *src2_data, size_t src2_step, uchar *dst_data, size_t dst_step, int width, int height, int operation); @@ -373,6 +377,16 @@ static inline int kleidicv_sobel_with_fallback( #undef cv_hal_sobel #define cv_hal_sobel kleidicv_sobel_with_fallback +static inline int kleidicv_median_blur_with_fallback( + const uchar *src_data, size_t src_step, uchar *dst_data, size_t dst_step, + int width, int height, int depth, int cn, int ksize) { + return KLEIDICV_HAL_FALLBACK_FORWARD(medianBlur, cv_hal_medianBlur, src_data, + src_step, dst_data, dst_step, width, + height, depth, cn, ksize); +} +#undef cv_hal_medianBlur +#define cv_hal_medianBlur kleidicv_median_blur_with_fallback + #if KLEIDICV_EXPERIMENTAL_FEATURE_CANNY // canny static inline int kleidicv_canny_with_fallback( diff --git a/benchmark/benchmark.cpp b/benchmark/benchmark.cpp index 94f8dd46b5fcdbd7143b347e3ff28e573e8602e1..3b85ead262a1cc991a433b7c491515c287222b0f 100644 --- a/benchmark/benchmark.cpp +++ b/benchmark/benchmark.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 Arm Limited and/or its affiliates +// SPDX-FileCopyrightText: 2024 - 2025 Arm Limited and/or its affiliates // // SPDX-License-Identifier: Apache-2.0 @@ -359,6 +359,58 @@ BENCH_GAUSSIAN_BLUR(7, 3); BENCH_GAUSSIAN_BLUR(15, 1); BENCH_GAUSSIAN_BLUR(15, 3); +template +static void median_blur(benchmark::State& state, Function func) { + int kernel_size = state.range(0); + + bench_functor(state, [&]() { + (void)func( + get_source_buffer_a(), image_width * Channels * sizeof(T), + get_destination_buffer(), + image_width * Channels * sizeof(T), image_width, image_height, Channels, + kernel_size, kernel_size, KLEIDICV_BORDER_TYPE_REPLICATE); + }); +} + +BENCHMARK_TEMPLATE2_CAPTURE(median_blur, uint8_t, 1, , kleidicv_median_blur_u8) + ->Arg(5); +BENCHMARK_TEMPLATE2_CAPTURE(median_blur, uint8_t, 4, , kleidicv_median_blur_u8) + ->Arg(5); + +BENCHMARK_TEMPLATE2_CAPTURE(median_blur, int8_t, 1, , kleidicv_median_blur_s8) + ->Arg(5); +BENCHMARK_TEMPLATE2_CAPTURE(median_blur, int8_t, 4, , kleidicv_median_blur_s8) + ->Arg(5); + +BENCHMARK_TEMPLATE2_CAPTURE(median_blur, uint16_t, 1, , + kleidicv_median_blur_u16) + ->Arg(5); +BENCHMARK_TEMPLATE2_CAPTURE(median_blur, uint16_t, 4, , + kleidicv_median_blur_u16) + ->Arg(5); + +BENCHMARK_TEMPLATE2_CAPTURE(median_blur, int16_t, 1, , kleidicv_median_blur_s16) + ->Arg(5); +BENCHMARK_TEMPLATE2_CAPTURE(median_blur, int16_t, 4, , kleidicv_median_blur_s16) + ->Arg(5); + +BENCHMARK_TEMPLATE2_CAPTURE(median_blur, uint32_t, 1, , + kleidicv_median_blur_u32) + ->Arg(5); +BENCHMARK_TEMPLATE2_CAPTURE(median_blur, uint32_t, 4, , + kleidicv_median_blur_u32) + ->Arg(5); + +BENCHMARK_TEMPLATE2_CAPTURE(median_blur, int32_t, 1, , kleidicv_median_blur_s32) + ->Arg(5); +BENCHMARK_TEMPLATE2_CAPTURE(median_blur, int32_t, 4, , kleidicv_median_blur_s32) + ->Arg(5); + +BENCHMARK_TEMPLATE2_CAPTURE(median_blur, float, 1, , kleidicv_median_blur_f32) + ->Arg(5); +BENCHMARK_TEMPLATE2_CAPTURE(median_blur, float, 4, , kleidicv_median_blur_f32) + ->Arg(5); + template static void sobel_filter(Function f, benchmark::State& state) { bench_functor(state, [f]() { diff --git a/conformity/opencv/test_median_blur.cpp b/conformity/opencv/test_median_blur.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3b1da2defd8e74a402a489b1a5c1bb4539b4649d --- /dev/null +++ b/conformity/opencv/test_median_blur.cpp @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2024 - 2025 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "tests.h" + +cv::Mat exec_median_blur(cv::Mat& input) { + cv::Mat result; + cv::medianBlur(input, result, 5); + return result; +} + +#if MANAGER +template +bool test_median_blur(int index, RecreatedMessageQueue& request_queue, + RecreatedMessageQueue& reply_queue) { + cv::RNG rng(0); + + for (size_t x = 5; x <= 16; ++x) { + for (size_t y = 5; y <= 16; ++y) { + cv::Mat input(x, y, get_opencv_matrix_type()); + rng.fill(input, cv::RNG::UNIFORM, std::numeric_limits::min(), + std::numeric_limits::max()); + + cv::Mat actual = exec_median_blur(input); + cv::Mat expected = get_expected_from_subordinate(index, request_queue, + reply_queue, input); + + if (are_matrices_different(0, actual, expected)) { + fail_print_matrices(x, y, input, actual, expected); + return true; + } + } + } + + return false; +} +#endif + +std::vector& median_blur_tests_get() { + // clang-format off + static std::vector tests = { + TEST("Median 5x5, 1 channel (U8)", (test_median_blur), exec_median_blur), + TEST("Median 5x5, 3 channel (U8)", (test_median_blur), exec_median_blur), + TEST("Median 5x5, 4 channel (U8)", (test_median_blur), exec_median_blur), + TEST("Median 5x5, 1 channel (U16)", (test_median_blur), exec_median_blur), + TEST("Median 5x5, 3 channel (U16)", (test_median_blur), exec_median_blur), + TEST("Median 5x5, 4 channel (U16)", (test_median_blur), exec_median_blur), + TEST("Median 5x5, 1 channel (S16)", (test_median_blur), exec_median_blur), + TEST("Median 5x5, 3 channel (S16)", (test_median_blur), exec_median_blur), + TEST("Median 5x5, 4 channel (S16)", (test_median_blur), exec_median_blur), + TEST("Median 5x5, 1 channel (F32)", (test_median_blur), exec_median_blur), + TEST("Median 5x5, 3 channel (F32)", (test_median_blur), exec_median_blur), + TEST("Median 5x5, 4 channel (F32)", (test_median_blur), exec_median_blur), + }; + // clang-format on + return tests; +} diff --git a/conformity/opencv/tests.cpp b/conformity/opencv/tests.cpp index 614b66cb1467d3edcc3e838d6dbba25b983c6f88..55744af48793171c5a0792a859538cf6c354a101 100644 --- a/conformity/opencv/tests.cpp +++ b/conformity/opencv/tests.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 Arm Limited and/or its affiliates +// SPDX-FileCopyrightText: 2024 - 2025 Arm Limited and/or its affiliates // // SPDX-License-Identifier: Apache-2.0 @@ -44,6 +44,7 @@ std::vector all_tests = merge_tests({ warp_perspective_tests_get, blur_and_downsample_tests_get, scharr_interleaved_tests_get, + median_blur_tests_get, }); #if MANAGER diff --git a/conformity/opencv/tests.h b/conformity/opencv/tests.h index 6087026f2c333bb24165e4a844cd5e26d0f35a3c..b7d4238b8e2965a6effae11e8c35cf1338a4da1e 100644 --- a/conformity/opencv/tests.h +++ b/conformity/opencv/tests.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 Arm Limited and/or its affiliates +// SPDX-FileCopyrightText: 2024 - 2025 Arm Limited and/or its affiliates // // SPDX-License-Identifier: Apache-2.0 @@ -28,5 +28,6 @@ std::vector& remap_tests_get(); std::vector& warp_perspective_tests_get(); std::vector& blur_and_downsample_tests_get(); std::vector& scharr_interleaved_tests_get(); +std::vector& median_blur_tests_get(); #endif // KLEIDICV_OPENCV_CONFORMITY_TESTS_H_ diff --git a/conformity/opencv/utils.h b/conformity/opencv/utils.h index bf19e7b13f8e773575e073a7280bb72a9bd27fab..9c7059d52e8072c581b267e369a573786634d1a3 100644 --- a/conformity/opencv/utils.h +++ b/conformity/opencv/utils.h @@ -19,6 +19,8 @@ constexpr int get_opencv_matrix_type() { return CV_16UC(Channels); } else if constexpr (std::is_same_v) { return CV_16SC(Channels); + } else if constexpr (std::is_same_v) { + return CV_32FC(Channels); } } diff --git a/doc/functionality.md b/doc/functionality.md index 095a8d6b42a9f1d62ba3c33cc1898ce304a22c13..61926df934379f09649124755b194dc9f6e26535 100644 --- a/doc/functionality.md +++ b/doc/functionality.md @@ -72,13 +72,14 @@ See `doc/opencv.md` for details of the functionality available in OpenCV. | Rotate (90 degrees clockwise) | x | x | x | x | ## Image filters -| | u8 | s16 | u16 | -|--------------------------------------|-----|-----|-----| -| Erode | x | | | -| Dilate | x | | | -| Sobel (3x3) | x | | | -| Separable Filter 2D (5x5) | x | x | x | -| Gaussian Blur (3x3, 5x5, 7x7, 15x15) | x | | | +| | s8 | u8 | s16 | u16 | s32 | u32 | f32 | +|--------------------------------------|-----|-----|-----|-----|-----|-----|-----| +| Erode | | x | | | | | | +| Dilate | | x | | | | | | +| Sobel (3x3) | | x | | | | | | +| Separable Filter 2D (5x5) | | x | x | x | | | | +| Gaussian Blur (3x3, 5x5, 7x7, 15x15) | | x | | | | | | +| Median Blur (5x5) | x | x | x | x | x | x | x | ## Resize to quarter | | u8 | diff --git a/doc/opencv.md b/doc/opencv.md index 146d5162e2b9ad08beaae13698d76fd6d0015ccd..f62512c11b6b06bcdd3da4d22fb58230a5e05d41 100644 --- a/doc/opencv.md +++ b/doc/opencv.md @@ -157,6 +157,13 @@ Notes on parameters: * `delta` - delta value is not supported. * `borderType` - only supports [`cv::BORDER_REPLICATE`](https://docs.opencv.org/4.10.0/d2/de8/group__core__array.html#gga209f2f4869e304c82d07739337eae7c5aa1de4cff95e3377d6d0cbe7569bd4e9f). +### [`cv::medianBlur()`](https://docs.opencv.org/4.11.0/d4/d86/group__imgproc__filter.html#ga564869aa33e58769b4469101aac458f9) +Applies median filter to a given image. + +Notes on parameters: +* `src.cols`,`src.rows` - image width and height must be `>=5`. +* `ksize` - must be 5, as KleidiCV only supports 5x5 kernel size. + ### [`cv::transpose()`](https://docs.opencv.org/4.10.0/d2/de8/group__core__array.html#ga46630ed6c0ea6254a35f447289bd7404) Transposes a matrix. diff --git a/kleidicv/include/kleidicv/filters/median_blur.h b/kleidicv/include/kleidicv/filters/median_blur.h new file mode 100644 index 0000000000000000000000000000000000000000..8487734325af1f7f441ad6958918eced182e7788 --- /dev/null +++ b/kleidicv/include/kleidicv/filters/median_blur.h @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2023 - 2025 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#ifndef KLEIDICV_FILTERS_MEDIAN_BLUR_H +#define KLEIDICV_FILTERS_MEDIAN_BLUR_H + +#include + +#include "kleidicv/config.h" +#include "kleidicv/kleidicv.h" +#include "kleidicv/types.h" +#include "kleidicv/workspace/border_types.h" + +extern "C" { + +// For internal use only. See instead kleidicv_median_blur_stripe_s8. +// find a median across an image. The stripe is defined by the +// range (y_begin, y_end]. +KLEIDICV_API_DECLARATION(kleidicv_median_blur_stripe_s8, const int8_t *src, + size_t src_stride, int8_t *dst, size_t dst_stride, + size_t width, size_t height, size_t y_begin, + size_t y_end, size_t channels, size_t kernel_width, + size_t kernel_height, + kleidicv::FixedBorderType border_type); + +// For internal use only. See instead kleidicv_median_blur_stripe_u8. +// find a median across an image. The stripe is defined by the +// range (y_begin, y_end]. +KLEIDICV_API_DECLARATION(kleidicv_median_blur_stripe_u8, const uint8_t *src, + size_t src_stride, uint8_t *dst, size_t dst_stride, + size_t width, size_t height, size_t y_begin, + size_t y_end, size_t channels, size_t kernel_width, + size_t kernel_height, + kleidicv::FixedBorderType border_type); + +// For internal use only. See instead kleidicv_median_blur_stripe_s16. +// Filter a horizontal stripe across an image. The stripe is defined by the +// range (y_begin, y_end]. +KLEIDICV_API_DECLARATION(kleidicv_median_blur_stripe_s16, const int16_t *src, + size_t src_stride, int16_t *dst, size_t dst_stride, + size_t width, size_t height, size_t y_begin, + size_t y_end, size_t channels, size_t kernel_width, + size_t kernel_height, + kleidicv::FixedBorderType border_type); + +// For internal use only. See instead kleidicv_median_blur_stripe_u16. +// Filter a horizontal stripe across an image. The stripe is defined by the +// range (y_begin, y_end]. +KLEIDICV_API_DECLARATION(kleidicv_median_blur_stripe_u16, const uint16_t *src, + size_t src_stride, uint16_t *dst, size_t dst_stride, + size_t width, size_t height, size_t y_begin, + size_t y_end, size_t channels, size_t kernel_width, + size_t kernel_height, + kleidicv::FixedBorderType border_type); + +// For internal use only. See instead kleidicv_median_blur_stripe_s32. +// Filter a horizontal stripe across an image. The stripe is defined by the +// range (y_begin, y_end]. +KLEIDICV_API_DECLARATION(kleidicv_median_blur_stripe_s32, const int32_t *src, + size_t src_stride, int32_t *dst, size_t dst_stride, + size_t width, size_t height, size_t y_begin, + size_t y_end, size_t channels, size_t kernel_width, + size_t kernel_height, + kleidicv::FixedBorderType border_type); + +// For internal use only. See instead kleidicv_median_blur_stripe_u32. +// Filter a horizontal stripe across an image. The stripe is defined by the +// range (y_begin, y_end]. +KLEIDICV_API_DECLARATION(kleidicv_median_blur_stripe_u32, const uint32_t *src, + size_t src_stride, uint32_t *dst, size_t dst_stride, + size_t width, size_t height, size_t y_begin, + size_t y_end, size_t channels, size_t kernel_width, + size_t kernel_height, + kleidicv::FixedBorderType border_type); + +// For internal use only. See instead kleidicv_median_blur_stripe_f32. +// Filter a horizontal stripe across an image. The stripe is defined by the +// range (y_begin, y_end]. +KLEIDICV_API_DECLARATION(kleidicv_median_blur_stripe_f32, const float *src, + size_t src_stride, float *dst, size_t dst_stride, + size_t width, size_t height, size_t y_begin, + size_t y_end, size_t channels, size_t kernel_width, + size_t kernel_height, + kleidicv::FixedBorderType border_type); +} + +namespace kleidicv { +template +inline kleidicv_error_t check_ptrs_strides_imagesizes(const T *src, + size_t src_stride, T *dst, + size_t dst_stride, + size_t width, + size_t height) { + CHECK_POINTER_AND_STRIDE(src, src_stride, height); + CHECK_POINTER_AND_STRIDE(dst, dst_stride, height); + CHECK_IMAGE_SIZE(width, height); + return KLEIDICV_OK; +} +template +inline std::pair median_blur_is_implemented( + const T *src, size_t src_stride, T *dst, size_t dst_stride, size_t width, + size_t height, size_t channels, size_t kernel_width, size_t kernel_height, + kleidicv_border_type_t border_type) { + auto image_check = check_ptrs_strides_imagesizes(src, src_stride, dst, + dst_stride, width, height); + if (image_check != KLEIDICV_OK) { + return std::make_pair(image_check, FixedBorderType{}); + } + + auto fixed_border_type = kleidicv::get_fixed_border_type(border_type); + if ((src != dst) && (channels <= KLEIDICV_MAXIMUM_CHANNEL_COUNT) && + (kernel_width == kernel_height) && (height >= kernel_height - 1) && + (width >= kernel_width - 1) && (kernel_width == 5) && + fixed_border_type.has_value()) { + return std::make_pair(KLEIDICV_OK, *fixed_border_type); + } + + return std::make_pair(KLEIDICV_ERROR_NOT_IMPLEMENTED, FixedBorderType{}); +} + +} // namespace kleidicv + +#endif // KLEIDICV_FILTERS_MEDIAN_BLUR_H diff --git a/kleidicv/include/kleidicv/kleidicv.h b/kleidicv/include/kleidicv/kleidicv.h index 8c76910ad8c1a12271c91d1acd54d16a56d9ccb6..9644ac3bbb531059168b22c10bbfaf4b18084b19 100644 --- a/kleidicv/include/kleidicv/kleidicv.h +++ b/kleidicv/include/kleidicv/kleidicv.h @@ -2004,6 +2004,60 @@ kleidicv_error_t kleidicv_warp_perspective_u8( kleidicv_interpolation_type_t interpolation, kleidicv_border_type_t border_type, const uint8_t *border_value); +#define KLEIDICV_FILTER_OP_MEDIAN(name, type) \ + kleidicv_error_t name( \ + const type *src, size_t src_stride, type *dst, size_t dst_stride, \ + size_t width, size_t height, size_t channels, size_t kernel_width, \ + size_t kernel_height, kleidicv_border_type_t border_type) + +/// Reduces noise by applying a median filter. +/// +/// Width and height are assumed to be the same for the source and for the +/// destination. The total number of elements is limited to +/// @ref KLEIDICV_MAX_IMAGE_PIXELS. +/// +/// @param src Pointer to the source data. Must be non-null. +/// @param src_stride Distance in bytes from the start of one row to the +/// start of the next row in the source data. Must be a +/// multiple of `sizeof(type)` and no less than +/// `width * sizeof(type) * channels`, except for +/// single-row images. +/// @param dst Pointer to the destination data. Must be non-null. +/// @param dst_stride Distance in bytes from the start of one row to the +/// start of the next row in the destination data. Must be +/// a multiple of `sizeof(type)` and no less than +/// `width * sizeof(type) * channels`, except for +/// single-row images. +/// @param width Number of columns in the data. (One column consists of +/// `channels` number of elements.) Must be greater +/// than 4. +/// @param height Number of rows in the data. Must be greater than 4. +/// @param channels Number of channels in the data. Must not be more than +/// @ref KLEIDICV_MAXIMUM_CHANNEL_COUNT. +/// @param kernel_width Width of the Median kernel. Must be 5 and equal to +/// `kernel_height`. +/// @param kernel_height Height of the Median kernel. Must be 5 and equal to +/// `kernel_width`. +/// @param border_type Way of handling the border. The supported border types +/// are: \n +/// - @ref KLEIDICV_BORDER_TYPE_REPLICATE \n +/// - @ref KLEIDICV_BORDER_TYPE_REFLECT \n +/// - @ref KLEIDICV_BORDER_TYPE_WRAP \n +/// - @ref KLEIDICV_BORDER_TYPE_REVERSE +KLEIDICV_FILTER_OP_MEDIAN(kleidicv_median_blur_u8, uint8_t); +/// @copydoc kleidicv_median_blur_u8 +KLEIDICV_FILTER_OP_MEDIAN(kleidicv_median_blur_s16, int16_t); +/// @copydoc kleidicv_median_blur_u8 +KLEIDICV_FILTER_OP_MEDIAN(kleidicv_median_blur_u16, uint16_t); +/// @copydoc kleidicv_median_blur_u8 +KLEIDICV_FILTER_OP_MEDIAN(kleidicv_median_blur_f32, float); +/// @copydoc kleidicv_median_blur_u8 +KLEIDICV_FILTER_OP_MEDIAN(kleidicv_median_blur_s8, int8_t); +/// @copydoc kleidicv_median_blur_u8 +KLEIDICV_FILTER_OP_MEDIAN(kleidicv_median_blur_s32, int32_t); +/// @copydoc kleidicv_median_blur_u8 +KLEIDICV_FILTER_OP_MEDIAN(kleidicv_median_blur_u32, uint32_t); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/kleidicv/src/filters/median_blur_api.cpp b/kleidicv/src/filters/median_blur_api.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8263f7c95e5971fdafe8c8e940d32832874c94b7 --- /dev/null +++ b/kleidicv/src/filters/median_blur_api.cpp @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: 2023 - 2025 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#include "kleidicv/dispatch.h" +#include "kleidicv/filters/median_blur.h" +#include "kleidicv/kleidicv.h" +namespace kleidicv { + +namespace neon { + +template +kleidicv_error_t median_blur_stripe(const T *src, size_t src_stride, T *dst, + size_t dst_stride, size_t width, + size_t height, size_t y_begin, size_t y_end, + size_t channels, size_t kernel_width, + size_t kernel_height, + FixedBorderType border_type); + +} // namespace neon + +} // namespace kleidicv + +#define KLEIDICV_DEFINE_C_API(name, type) \ + KLEIDICV_MULTIVERSION_C_API(name, &kleidicv::neon::median_blur_stripe, \ + nullptr, nullptr) + +KLEIDICV_DEFINE_C_API(kleidicv_median_blur_stripe_s8, int8_t); +KLEIDICV_DEFINE_C_API(kleidicv_median_blur_stripe_u8, uint8_t); +KLEIDICV_DEFINE_C_API(kleidicv_median_blur_stripe_u16, uint16_t); +KLEIDICV_DEFINE_C_API(kleidicv_median_blur_stripe_s16, int16_t); +KLEIDICV_DEFINE_C_API(kleidicv_median_blur_stripe_u32, uint32_t); +KLEIDICV_DEFINE_C_API(kleidicv_median_blur_stripe_s32, int32_t); +KLEIDICV_DEFINE_C_API(kleidicv_median_blur_stripe_f32, float); + +extern "C" { + +kleidicv_error_t kleidicv_median_blur_s8(const int8_t *src, size_t src_stride, + int8_t *dst, size_t dst_stride, + size_t width, size_t height, + size_t channels, size_t kernel_width, + size_t kernel_height, + kleidicv_border_type_t border_type) { + auto [checks_result, fixed_border_type] = + kleidicv::median_blur_is_implemented( + src, src_stride, dst, dst_stride, width, height, channels, + kernel_width, kernel_height, border_type); + + if (checks_result != KLEIDICV_OK) { + return checks_result; + } + + return kleidicv_median_blur_stripe_s8( + src, src_stride, dst, dst_stride, width, height, 0, height, channels, + kernel_width, kernel_height, fixed_border_type); +} + +kleidicv_error_t kleidicv_median_blur_u8(const uint8_t *src, size_t src_stride, + uint8_t *dst, size_t dst_stride, + size_t width, size_t height, + size_t channels, size_t kernel_width, + size_t kernel_height, + kleidicv_border_type_t border_type) { + auto [checks_result, fixed_border_type] = + kleidicv::median_blur_is_implemented( + src, src_stride, dst, dst_stride, width, height, channels, + kernel_width, kernel_height, border_type); + + if (checks_result != KLEIDICV_OK) { + return checks_result; + } + + return kleidicv_median_blur_stripe_u8( + src, src_stride, dst, dst_stride, width, height, 0, height, channels, + kernel_width, kernel_height, fixed_border_type); +} + +kleidicv_error_t kleidicv_median_blur_s16(const int16_t *src, size_t src_stride, + int16_t *dst, size_t dst_stride, + size_t width, size_t height, + size_t channels, size_t kernel_width, + size_t kernel_height, + kleidicv_border_type_t border_type) { + auto [checks_result, fixed_border_type] = + kleidicv::median_blur_is_implemented( + src, src_stride, dst, dst_stride, width, height, channels, + kernel_width, kernel_height, border_type); + + if (checks_result != KLEIDICV_OK) { + return checks_result; + } + + return kleidicv_median_blur_stripe_s16( + src, src_stride, dst, dst_stride, width, height, 0, height, channels, + kernel_width, kernel_height, fixed_border_type); +} + +kleidicv_error_t kleidicv_median_blur_u16( + const uint16_t *src, size_t src_stride, uint16_t *dst, size_t dst_stride, + size_t width, size_t height, size_t channels, size_t kernel_width, + size_t kernel_height, kleidicv_border_type_t border_type) { + auto [checks_result, fixed_border_type] = + kleidicv::median_blur_is_implemented( + src, src_stride, dst, dst_stride, width, height, channels, + kernel_width, kernel_height, border_type); + + if (checks_result != KLEIDICV_OK) { + return checks_result; + } + + return kleidicv_median_blur_stripe_u16( + src, src_stride, dst, dst_stride, width, height, 0, height, channels, + kernel_width, kernel_height, fixed_border_type); +} + +kleidicv_error_t kleidicv_median_blur_s32(const int32_t *src, size_t src_stride, + int32_t *dst, size_t dst_stride, + size_t width, size_t height, + size_t channels, size_t kernel_width, + size_t kernel_height, + kleidicv_border_type_t border_type) { + auto [checks_result, fixed_border_type] = + kleidicv::median_blur_is_implemented( + src, src_stride, dst, dst_stride, width, height, channels, + kernel_width, kernel_height, border_type); + + if (checks_result != KLEIDICV_OK) { + return checks_result; + } + + return kleidicv_median_blur_stripe_s32( + src, src_stride, dst, dst_stride, width, height, 0, height, channels, + kernel_width, kernel_height, fixed_border_type); +} + +kleidicv_error_t kleidicv_median_blur_u32( + const uint32_t *src, size_t src_stride, uint32_t *dst, size_t dst_stride, + size_t width, size_t height, size_t channels, size_t kernel_width, + size_t kernel_height, kleidicv_border_type_t border_type) { + auto [checks_result, fixed_border_type] = + kleidicv::median_blur_is_implemented( + src, src_stride, dst, dst_stride, width, height, channels, + kernel_width, kernel_height, border_type); + + if (checks_result != KLEIDICV_OK) { + return checks_result; + } + + return kleidicv_median_blur_stripe_u32( + src, src_stride, dst, dst_stride, width, height, 0, height, channels, + kernel_width, kernel_height, fixed_border_type); +} + +kleidicv_error_t kleidicv_median_blur_f32(const float *src, size_t src_stride, + float *dst, size_t dst_stride, + size_t width, size_t height, + size_t channels, size_t kernel_width, + size_t kernel_height, + kleidicv_border_type_t border_type) { + auto [checks_result, fixed_border_type] = + kleidicv::median_blur_is_implemented( + src, src_stride, dst, dst_stride, width, height, channels, + kernel_width, kernel_height, border_type); + + if (checks_result != KLEIDICV_OK) { + return checks_result; + } + + return kleidicv_median_blur_stripe_f32( + src, src_stride, dst, dst_stride, width, height, 0, height, channels, + kernel_width, kernel_height, fixed_border_type); +} + +} // extern "C" diff --git a/kleidicv/src/filters/median_blur_neon.cpp b/kleidicv/src/filters/median_blur_neon.cpp new file mode 100644 index 0000000000000000000000000000000000000000..769873646c1a3e25e583df3d5348cd7435becdb5 --- /dev/null +++ b/kleidicv/src/filters/median_blur_neon.cpp @@ -0,0 +1,423 @@ +// SPDX-FileCopyrightText: 2023 - 2025 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#include "kleidicv/ctypes.h" +#include "kleidicv/filters/median_blur.h" +#include "kleidicv/kleidicv.h" +#include "kleidicv/neon.h" +#include "kleidicv/workspace/border_5x5.h" + +namespace kleidicv::neon { + +// Primary template for Median Blur filters. +template +class MedianBlur; + +// Template for Median Blur 5x5 filters. +template +class MedianBlur { + public: + using SourceType = ScalarType; + using DestinationType = SourceType; + using SourceVectorType = typename VecTraits::VectorType; + using DestinationVectorType = typename VecTraits::VectorType; + + void vector_path(SourceVectorType window[5][5], + DestinationVectorType& dst) const { + sorting_network5x5(window, dst); + } + + void scalar_path(SourceType window[5][5], DestinationType& dst) const { + sorting_network5x5(window, dst); + } + + private: + class vectorized_comparator { + public: + static void compare_and_swap(SourceVectorType& left, + SourceVectorType& right) { + SourceVectorType tmp_left = vmaxq(left, right); + SourceVectorType tmp_right = vminq(left, right); + left = tmp_left; + right = tmp_right; + } + + static void min(SourceVectorType& left, SourceVectorType& right) { + right = vminq(left, right); + } + + static void max(SourceVectorType& left, SourceVectorType& right) { + left = vmaxq(left, right); + } + }; + + class scalar_comparator { + public: + static void compare_and_swap(ScalarType& left, ScalarType& right) { + if (left < right) { + std::swap(left, right); + } + } + + static void min(ScalarType& left, ScalarType& right) { + right = std::min(left, right); + } + + static void max(ScalarType& left, ScalarType& right) { + left = std::max(left, right); + } + }; + + // R. B. Kent and M. S. Pattichis, ''Design of high-speed multiway merge + // sorting networks using fast single-stage N-sorters and N-filters,'' *IEEE + // Access*, vol. 10, pp. 79565–79581, Jul. 2022, + // doi: 10.1109/ACCESS.2022.3193370. The paper is currently available at: + // https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=9837930 + template + void sorting_network5x5(T window[5][5], T& dst) const { + // full sort col + // col 0 + Comparator::compare_and_swap(window[0][0], window[3][0]); + Comparator::compare_and_swap(window[1][0], window[4][0]); + Comparator::compare_and_swap(window[0][0], window[2][0]); + Comparator::compare_and_swap(window[1][0], window[3][0]); + Comparator::compare_and_swap(window[0][0], window[1][0]); + Comparator::compare_and_swap(window[2][0], window[4][0]); + Comparator::compare_and_swap(window[1][0], window[2][0]); + Comparator::compare_and_swap(window[3][0], window[4][0]); + Comparator::compare_and_swap(window[2][0], window[3][0]); + // col 1 + Comparator::compare_and_swap(window[0][1], window[3][1]); + Comparator::compare_and_swap(window[1][1], window[4][1]); + Comparator::compare_and_swap(window[0][1], window[2][1]); + Comparator::compare_and_swap(window[1][1], window[3][1]); + Comparator::compare_and_swap(window[0][1], window[1][1]); + Comparator::compare_and_swap(window[2][1], window[4][1]); + Comparator::compare_and_swap(window[1][1], window[2][1]); + Comparator::compare_and_swap(window[3][1], window[4][1]); + Comparator::compare_and_swap(window[2][1], window[3][1]); + // col 2 + Comparator::compare_and_swap(window[0][2], window[3][2]); + Comparator::compare_and_swap(window[1][2], window[4][2]); + Comparator::compare_and_swap(window[0][2], window[2][2]); + Comparator::compare_and_swap(window[1][2], window[3][2]); + Comparator::compare_and_swap(window[0][2], window[1][2]); + Comparator::compare_and_swap(window[2][2], window[4][2]); + Comparator::compare_and_swap(window[1][2], window[2][2]); + Comparator::compare_and_swap(window[3][2], window[4][2]); + Comparator::compare_and_swap(window[2][2], window[3][2]); + // col 3 + Comparator::compare_and_swap(window[0][3], window[3][3]); + Comparator::compare_and_swap(window[1][3], window[4][3]); + Comparator::compare_and_swap(window[0][3], window[2][3]); + Comparator::compare_and_swap(window[1][3], window[3][3]); + Comparator::compare_and_swap(window[0][3], window[1][3]); + Comparator::compare_and_swap(window[2][3], window[4][3]); + Comparator::compare_and_swap(window[1][3], window[2][3]); + Comparator::compare_and_swap(window[3][3], window[4][3]); + Comparator::compare_and_swap(window[2][3], window[3][3]); + // col 4 + Comparator::compare_and_swap(window[0][4], window[3][4]); + Comparator::compare_and_swap(window[1][4], window[4][4]); + Comparator::compare_and_swap(window[0][4], window[2][4]); + Comparator::compare_and_swap(window[1][4], window[3][4]); + Comparator::compare_and_swap(window[0][4], window[1][4]); + Comparator::compare_and_swap(window[2][4], window[4][4]); + Comparator::compare_and_swap(window[1][4], window[2][4]); + Comparator::compare_and_swap(window[3][4], window[4][4]); + Comparator::compare_and_swap(window[2][4], window[3][4]); + // partialy sort row + // sort row zero for only element 3 and 4 + Comparator::compare_and_swap(window[0][0], window[0][3]); + Comparator::compare_and_swap(window[0][1], window[0][4]); + Comparator::compare_and_swap(window[0][0], window[0][2]); + Comparator::compare_and_swap(window[0][1], window[0][3]); + Comparator::min(window[0][0], window[0][1]); + Comparator::compare_and_swap(window[0][2], window[0][4]); + Comparator::min(window[0][1], window[0][2]); + Comparator::compare_and_swap(window[0][3], window[0][4]); + Comparator::min(window[0][2], window[0][3]); + // sort row 1 for only element {2, 3, 4} + Comparator::compare_and_swap(window[1][0], window[1][3]); + Comparator::compare_and_swap(window[1][1], window[1][4]); + Comparator::compare_and_swap(window[1][0], window[1][2]); + Comparator::compare_and_swap(window[1][1], window[1][3]); + Comparator::min(window[1][0], window[1][1]); + Comparator::compare_and_swap(window[1][2], window[1][4]); + Comparator::min(window[1][1], window[1][2]); + Comparator::compare_and_swap(window[1][3], window[1][4]); + Comparator::compare_and_swap(window[1][2], window[1][3]); + // sort row 2 {1, 2, 3} + Comparator::compare_and_swap(window[2][0], window[2][3]); + Comparator::compare_and_swap(window[2][1], window[2][4]); + Comparator::compare_and_swap(window[2][0], window[2][2]); + Comparator::compare_and_swap(window[2][1], window[2][3]); + Comparator::min(window[2][0], window[2][1]); + Comparator::compare_and_swap(window[2][2], window[2][4]); + Comparator::compare_and_swap(window[2][1], window[2][2]); + Comparator::max(window[2][3], window[2][4]); + Comparator::compare_and_swap(window[2][2], window[2][3]); + // sort row 3 + Comparator::compare_and_swap(window[3][0], window[3][3]); + Comparator::compare_and_swap(window[3][1], window[3][4]); + Comparator::compare_and_swap(window[3][0], window[3][2]); + Comparator::compare_and_swap(window[3][1], window[3][3]); + Comparator::compare_and_swap(window[3][0], window[3][1]); + Comparator::compare_and_swap(window[3][2], window[3][4]); + Comparator::compare_and_swap(window[3][1], window[3][2]); + Comparator::max(window[3][3], window[3][4]); + Comparator::max(window[3][2], window[3][3]); + // sort row 4 + Comparator::compare_and_swap(window[4][0], window[4][3]); + Comparator::compare_and_swap(window[4][1], window[4][4]); + Comparator::compare_and_swap(window[4][0], window[4][2]); + Comparator::max(window[4][1], window[4][3]); + Comparator::compare_and_swap(window[4][0], window[4][1]); + Comparator::max(window[4][2], window[4][4]); + Comparator::max(window[4][1], window[4][2]); + // partialy sort digonal + // sort dig 0 + Comparator::min(window[0][3], window[2][1]); + Comparator::min(window[1][2], window[3][0]); + Comparator::min(window[2][1], window[3][0]); + // sort dig 1 + Comparator::compare_and_swap(window[0][4], window[3][1]); + Comparator::compare_and_swap(window[1][3], window[4][0]); + Comparator::compare_and_swap(window[0][4], window[2][2]); + Comparator::compare_and_swap(window[1][3], window[3][1]); + Comparator::min(window[0][4], window[1][3]); + Comparator::compare_and_swap(window[2][2], window[4][0]); + Comparator::min(window[1][3], window[2][2]); + Comparator::max(window[3][1], window[4][0]); + Comparator::max(window[2][2], window[3][1]); + // sort dig 2 + Comparator::max(window[1][4], window[3][2]); + Comparator::max(window[2][3], window[4][1]); + Comparator::max(window[1][4], window[2][3]); + // find median + Comparator::compare_and_swap(window[1][4], window[3][0]); + Comparator::min(window[1][4], window[2][2]); + Comparator::max(window[2][2], window[3][0]); + dst = window[2][2]; + } +}; // end of class MedianBlur + +// Primary Template for Filter 2D. +template +class Filter2D; + +// Template for Filter2D 5x5. +template +class Filter2D { + public: + using SourceType = typename FilterType::SourceType; + using DestinationType = typename FilterType::DestinationType; + using SourceVecTraits = typename neon::VecTraits; + using SourceVectorType = typename SourceVecTraits::VectorType; + using BorderInfoType = + typename ::KLEIDICV_TARGET_NAMESPACE::FixedBorderInfo5x5; + using BorderType = FixedBorderType; + using BorderOffsets = typename BorderInfoType::Offsets; + static constexpr size_t margin = 2UL; + explicit Filter2D(FilterType filter) : filter_{filter} {} + + void process_pixels_without_horizontal_borders( + size_t width, Rows src_rows, Rows dst_rows, + BorderOffsets window_row_offsets, + BorderOffsets window_col_offsets) const { + LoopUnroll2 loop{width * src_rows.channels(), + SourceVecTraits::num_lanes()}; + + loop.unroll_once([&](size_t index) { + SourceVectorType src[5][5]; + SourceVectorType dst_vec; + + auto load_array_element = [](const SourceType& x) { return vld1q(&x); }; + load_window(src, load_array_element, src_rows, window_row_offsets, + window_col_offsets, index); + filter_.vector_path(src, dst_vec); + + vst1q(&dst_rows[index], dst_vec); + }); + + loop.tail([&](size_t index) { + process_one_element_with_or_without_horizontal_borders( + src_rows, dst_rows, window_row_offsets, window_col_offsets, index); + }); + } + + void process_one_pixel_with_horizontal_borders( + Rows src_rows, Rows dst_rows, + BorderOffsets window_row_offsets, + BorderOffsets window_col_offsets) const { + for (size_t index = 0; index < src_rows.channels(); ++index) { + disable_loop_vectorization(); + process_one_element_with_or_without_horizontal_borders( + src_rows, dst_rows, window_row_offsets, window_col_offsets, index); + } + } + + private: + template + void load_window(T src[5][5], LoadArrayElementFunctionType load_array_element, + Rows src_rows, + BorderOffsets window_row_offsets, + BorderOffsets window_col_offsets, size_t index) const { + src[0][0] = load_array_element( + src_rows.at(window_row_offsets.c0(), window_col_offsets.c0())[index]); + src[0][1] = load_array_element( + src_rows.at(window_row_offsets.c0(), window_col_offsets.c1())[index]); + src[0][2] = load_array_element( + src_rows.at(window_row_offsets.c0(), window_col_offsets.c2())[index]); + src[0][3] = load_array_element( + src_rows.at(window_row_offsets.c0(), window_col_offsets.c3())[index]); + src[0][4] = load_array_element( + src_rows.at(window_row_offsets.c0(), window_col_offsets.c4())[index]); + src[1][0] = load_array_element( + src_rows.at(window_row_offsets.c1(), window_col_offsets.c0())[index]); + src[1][1] = load_array_element( + src_rows.at(window_row_offsets.c1(), window_col_offsets.c1())[index]); + src[1][2] = load_array_element( + src_rows.at(window_row_offsets.c1(), window_col_offsets.c2())[index]); + src[1][3] = load_array_element( + src_rows.at(window_row_offsets.c1(), window_col_offsets.c3())[index]); + src[1][4] = load_array_element( + src_rows.at(window_row_offsets.c1(), window_col_offsets.c4())[index]); + src[2][0] = load_array_element( + src_rows.at(window_row_offsets.c2(), window_col_offsets.c0())[index]); + src[2][1] = load_array_element( + src_rows.at(window_row_offsets.c2(), window_col_offsets.c1())[index]); + src[2][2] = load_array_element( + src_rows.at(window_row_offsets.c2(), window_col_offsets.c2())[index]); + src[2][3] = load_array_element( + src_rows.at(window_row_offsets.c2(), window_col_offsets.c3())[index]); + src[2][4] = load_array_element( + src_rows.at(window_row_offsets.c2(), window_col_offsets.c4())[index]); + src[3][0] = load_array_element( + src_rows.at(window_row_offsets.c3(), window_col_offsets.c0())[index]); + src[3][1] = load_array_element( + src_rows.at(window_row_offsets.c3(), window_col_offsets.c1())[index]); + src[3][2] = load_array_element( + src_rows.at(window_row_offsets.c3(), window_col_offsets.c2())[index]); + src[3][3] = load_array_element( + src_rows.at(window_row_offsets.c3(), window_col_offsets.c3())[index]); + src[3][4] = load_array_element( + src_rows.at(window_row_offsets.c3(), window_col_offsets.c4())[index]); + src[4][0] = load_array_element( + src_rows.at(window_row_offsets.c4(), window_col_offsets.c0())[index]); + src[4][1] = load_array_element( + src_rows.at(window_row_offsets.c4(), window_col_offsets.c1())[index]); + src[4][2] = load_array_element( + src_rows.at(window_row_offsets.c4(), window_col_offsets.c2())[index]); + src[4][3] = load_array_element( + src_rows.at(window_row_offsets.c4(), window_col_offsets.c3())[index]); + src[4][4] = load_array_element( + src_rows.at(window_row_offsets.c4(), window_col_offsets.c4())[index]); + } + + void process_one_element_with_or_without_horizontal_borders( + Rows src_rows, Rows dst_rows, + BorderOffsets window_row_offsets, BorderOffsets window_col_offsets, + size_t index) const { + SourceType src[5][5]; + + auto load_array_element = [](const SourceType& x) { return x; }; + load_window(src, load_array_element, src_rows, window_row_offsets, + window_col_offsets, index); + + filter_.scalar_path(src, dst_rows[index]); + } + FilterType filter_; +}; // end of class Filter2D + +template +void process_filter2d(Rectangle rect, size_t y_begin, size_t y_end, + Rows src_rows, + Rows dst_rows, + typename FilterType::BorderType border_type, + FilterType filter) { + // Border helper which calculates border offsets. + typename FilterType::BorderInfoType vertical_border{rect.height(), + border_type}; + typename FilterType::BorderInfoType horizontal_border{rect.width(), + border_type}; + + for (size_t vertical_index = y_begin; vertical_index < y_end; + ++vertical_index) { + auto vertical_offsets = vertical_border.offsets_with_border(vertical_index); + constexpr size_t margin = filter.margin; + + // Process data affected by left border. + KLEIDICV_FORCE_LOOP_UNROLL + for (size_t horizontal_index = 0; horizontal_index < margin; + ++horizontal_index) { + auto horizontal_offsets = + horizontal_border.offsets_with_left_border(horizontal_index); + filter.process_one_pixel_with_horizontal_borders( + src_rows.at(vertical_index, horizontal_index), + dst_rows.at(vertical_index, horizontal_index), vertical_offsets, + horizontal_offsets); + } + + // Process data which is not affected by any borders in bulk. + size_t width_without_borders = rect.width() - (2 * margin); + auto horizontal_offsets = horizontal_border.offsets_without_border(); + filter.process_pixels_without_horizontal_borders( + width_without_borders, src_rows.at(vertical_index, margin), + dst_rows.at(vertical_index, margin), vertical_offsets, + horizontal_offsets); + + // Process data affected by right border. + KLEIDICV_FORCE_LOOP_UNROLL + for (size_t horizontal_index = 0; horizontal_index < margin; + ++horizontal_index) { + size_t index = rect.width() - margin + horizontal_index; + auto horizontal_offsets = + horizontal_border.offsets_with_right_border(index); + filter.process_one_pixel_with_horizontal_borders( + src_rows.at(vertical_index, index), + dst_rows.at(vertical_index, index), vertical_offsets, + horizontal_offsets); + } + } +} + +// Shorthand for 5x5 2D filters driver type. +template +using Filter2D5x5 = Filter2D; + +template +kleidicv_error_t median_blur_stripe(const T* src, size_t src_stride, T* dst, + size_t dst_stride, size_t width, + size_t height, size_t y_begin, size_t y_end, + size_t channels, + [[maybe_unused]] size_t kernel_width, + [[maybe_unused]] size_t kernel_height, + FixedBorderType border_type) { + Rectangle rect{width, height}; + Rows src_rows{src, src_stride, channels}; + Rows dst_rows{dst, dst_stride, channels}; + MedianBlur median_filter; + Filter2D5x5> filter{median_filter}; + process_filter2d(rect, y_begin, y_end, src_rows, dst_rows, border_type, + filter); + return KLEIDICV_OK; +} + +#define KLEIDICV_INSTANTIATE_TEMPLATE(type) \ + template KLEIDICV_TARGET_FN_ATTRS kleidicv_error_t median_blur_stripe( \ + const type* src, size_t src_stride, type* dst, size_t dst_stride, \ + size_t width, size_t height, size_t y_begin, size_t y_end, \ + size_t channels, size_t kernel_width, size_t kernel_height, \ + FixedBorderType border_type) + +KLEIDICV_INSTANTIATE_TEMPLATE(int8_t); +KLEIDICV_INSTANTIATE_TEMPLATE(uint8_t); +KLEIDICV_INSTANTIATE_TEMPLATE(int16_t); +KLEIDICV_INSTANTIATE_TEMPLATE(uint16_t); +KLEIDICV_INSTANTIATE_TEMPLATE(int32_t); +KLEIDICV_INSTANTIATE_TEMPLATE(uint32_t); +KLEIDICV_INSTANTIATE_TEMPLATE(float); + +} // namespace kleidicv::neon diff --git a/kleidicv_thread/include/kleidicv_thread/kleidicv_thread.h b/kleidicv_thread/include/kleidicv_thread/kleidicv_thread.h index 76b7a8bae360e3490e7d3c1ab6d24599551033b4..5e14ab8f7aff169eb53e48c601495ce160bddb7b 100644 --- a/kleidicv_thread/include/kleidicv_thread/kleidicv_thread.h +++ b/kleidicv_thread/include/kleidicv_thread/kleidicv_thread.h @@ -337,6 +337,46 @@ kleidicv_error_t kleidicv_thread_sobel_3x3_horizontal_s16_u8( size_t width, size_t height, size_t channels, kleidicv_thread_multithreading); +/// Internal - not part of the public API and its direct use is not supported. +/// +/// Multithreaded implementation of kleidicv_median_blur_u8 - see +/// the documentation of that function for more details. +kleidicv_error_t kleidicv_thread_median_blur_u8( + const uint8_t *src, size_t src_stride, uint8_t *dst, size_t dst_stride, + size_t width, size_t height, size_t channels, size_t kernel_width, + size_t kernel_height, kleidicv_border_type_t border_type, + kleidicv_thread_multithreading); + +/// Internal - not part of the public API and its direct use is not supported. +/// +/// Multithreaded implementation of kleidicv_median_blur_s16 - see +/// the documentation of that function for more details. +kleidicv_error_t kleidicv_thread_median_blur_s16( + const int16_t *src, size_t src_stride, int16_t *dst, size_t dst_stride, + size_t width, size_t height, size_t channels, size_t kernel_width, + size_t kernel_height, kleidicv_border_type_t border_type, + kleidicv_thread_multithreading mt); + +/// Internal - not part of the public API and its direct use is not supported. +/// +/// Multithreaded implementation of kleidicv_median_blur_u16 - see +/// the documentation of that function for more details. +kleidicv_error_t kleidicv_thread_median_blur_u16( + const uint16_t *src, size_t src_stride, uint16_t *dst, size_t dst_stride, + size_t width, size_t height, size_t channels, size_t kernel_width, + size_t kernel_height, kleidicv_border_type_t border_type, + kleidicv_thread_multithreading mt); + +/// Internal - not part of the public API and its direct use is not supported. +/// +/// Multithreaded implementation of kleidicv_median_blur_f32 - see +/// the documentation of that function for more details. +kleidicv_error_t kleidicv_thread_median_blur_f32( + const float *src, size_t src_stride, float *dst, size_t dst_stride, + size_t width, size_t height, size_t channels, size_t kernel_width, + size_t kernel_height, kleidicv_border_type_t border_type, + kleidicv_thread_multithreading mt); + /// Internal - not part of the public API and its direct use is not supported. /// /// Multithreaded implementation of kleidicv_sobel_3x3_vertical_s16_u8 - see the diff --git a/kleidicv_thread/src/kleidicv_thread.cpp b/kleidicv_thread/src/kleidicv_thread.cpp index e71b2a339b8ecd27a58e9d086670253ffebf5ae9..8f413db0f3b0c92ab2e6287100e1f68f1c8e9476 100644 --- a/kleidicv_thread/src/kleidicv_thread.cpp +++ b/kleidicv_thread/src/kleidicv_thread.cpp @@ -16,6 +16,7 @@ #include "kleidicv/ctypes.h" #include "kleidicv/filters/blur_and_downsample.h" #include "kleidicv/filters/gaussian_blur.h" +#include "kleidicv/filters/median_blur.h" #include "kleidicv/filters/scharr.h" #include "kleidicv/filters/separable_filter_2d.h" #include "kleidicv/filters/sobel.h" @@ -577,6 +578,98 @@ kleidicv_error_t kleidicv_thread_sobel_3x3_horizontal_s16_u8( return parallel_batches(callback, mt, height); } +kleidicv_error_t kleidicv_thread_median_blur_u8( + const uint8_t *src, size_t src_stride, uint8_t *dst, size_t dst_stride, + size_t width, size_t height, size_t channels, size_t kernel_width, + size_t kernel_height, kleidicv_border_type_t border_type, + kleidicv_thread_multithreading mt) { + auto result_pair = kleidicv::median_blur_is_implemented( + src, src_stride, dst, dst_stride, width, height, channels, kernel_width, + kernel_height, border_type); + + auto checks_result = result_pair.first; + auto fixed_border_type = result_pair.second; + if (checks_result != KLEIDICV_OK) { + return checks_result; + } + + auto callback = [=](unsigned y_begin, unsigned y_end) { + return kleidicv_median_blur_stripe_u8( + src, src_stride, dst, dst_stride, width, height, y_begin, y_end, + channels, kernel_width, kernel_height, fixed_border_type); + }; + return parallel_batches(callback, mt, height); +} + +kleidicv_error_t kleidicv_thread_median_blur_s16( + const int16_t *src, size_t src_stride, int16_t *dst, size_t dst_stride, + size_t width, size_t height, size_t channels, size_t kernel_width, + size_t kernel_height, kleidicv_border_type_t border_type, + kleidicv_thread_multithreading mt) { + auto result_pair = kleidicv::median_blur_is_implemented( + src, src_stride, dst, dst_stride, width, height, channels, kernel_width, + kernel_height, border_type); + + auto checks_result = result_pair.first; + auto fixed_border_type = result_pair.second; + if (checks_result != KLEIDICV_OK) { + return checks_result; + } + + auto callback = [=](unsigned y_begin, unsigned y_end) { + return kleidicv_median_blur_stripe_s16( + src, src_stride, dst, dst_stride, width, height, y_begin, y_end, + channels, kernel_width, kernel_height, fixed_border_type); + }; + return parallel_batches(callback, mt, height); +} + +kleidicv_error_t kleidicv_thread_median_blur_u16( + const uint16_t *src, size_t src_stride, uint16_t *dst, size_t dst_stride, + size_t width, size_t height, size_t channels, size_t kernel_width, + size_t kernel_height, kleidicv_border_type_t border_type, + kleidicv_thread_multithreading mt) { + auto result_pair = kleidicv::median_blur_is_implemented( + src, src_stride, dst, dst_stride, width, height, channels, kernel_width, + kernel_height, border_type); + + auto checks_result = result_pair.first; + auto fixed_border_type = result_pair.second; + if (checks_result != KLEIDICV_OK) { + return checks_result; + } + + auto callback = [=](unsigned y_begin, unsigned y_end) { + return kleidicv_median_blur_stripe_u16( + src, src_stride, dst, dst_stride, width, height, y_begin, y_end, + channels, kernel_width, kernel_height, fixed_border_type); + }; + return parallel_batches(callback, mt, height); +} + +kleidicv_error_t kleidicv_thread_median_blur_f32( + const float *src, size_t src_stride, float *dst, size_t dst_stride, + size_t width, size_t height, size_t channels, size_t kernel_width, + size_t kernel_height, kleidicv_border_type_t border_type, + kleidicv_thread_multithreading mt) { + auto result_pair = kleidicv::median_blur_is_implemented( + src, src_stride, dst, dst_stride, width, height, channels, kernel_width, + kernel_height, border_type); + + auto checks_result = result_pair.first; + auto fixed_border_type = result_pair.second; + if (checks_result != KLEIDICV_OK) { + return checks_result; + } + + auto callback = [=](unsigned y_begin, unsigned y_end) { + return kleidicv_median_blur_stripe_f32( + src, src_stride, dst, dst_stride, width, height, y_begin, y_end, + channels, kernel_width, kernel_height, fixed_border_type); + }; + return parallel_batches(callback, mt, height); +} + kleidicv_error_t kleidicv_thread_sobel_3x3_vertical_s16_u8( const uint8_t *src, size_t src_stride, int16_t *dst, size_t dst_stride, size_t width, size_t height, size_t channels, diff --git a/scripts/benchmark/benchmarks.txt b/scripts/benchmark/benchmarks.txt index 158505944652653397d7bf1701771ccf717a4632..128529bb8fb360e9639849380eed569ef3a85a3b 100755 --- a/scripts/benchmark/benchmarks.txt +++ b/scripts/benchmark/benchmarks.txt @@ -31,6 +31,8 @@ SepFilter2D_5x5_U8: opencv_perf_imgproc '*KleidiCV_SepFilter2D.SepFilter2D/*' ' SepFilter2D_5x5_U16: opencv_perf_imgproc '*KleidiCV_SepFilter2D.SepFilter2D/*' '($PIXEL_FORMAT, 16UC1, 5, BORDER_REPLICATE)' SepFilter2D_5x5_S16: opencv_perf_imgproc '*KleidiCV_SepFilter2D.SepFilter2D/*' '($PIXEL_FORMAT, 16SC1, 5, BORDER_REPLICATE)' +MedianBlur5x5: opencv_perf_imgproc '*medianBlur/*' '($PIXEL_FORMAT, 8UC1, 5)' + GaussianBlur3x3: opencv_perf_imgproc '*gaussianBlur3x3/*' '($PIXEL_FORMAT, 8UC1, BORDER_REPLICATE)' GaussianBlur5x5: opencv_perf_imgproc '*gaussianBlur5x5/*' '($PIXEL_FORMAT, 8UC1, BORDER_REPLICATE)' GaussianBlur7x7: opencv_perf_imgproc '*gaussianBlur7x7/*' '($PIXEL_FORMAT, 8UC1, BORDER_REPLICATE)' diff --git a/scripts/ci-opencv.sh b/scripts/ci-opencv.sh index b5343b1ac334757808fd90df202a4993286ea506..bad3718a143f7a00f363045082444a8a7704fec7 100755 --- a/scripts/ci-opencv.sh +++ b/scripts/ci-opencv.sh @@ -114,6 +114,7 @@ IMGPROC_TEST_PATTERNS=( '*Imgproc_Erode*' '*Imgproc_PyramidDown*' '*Imgproc_Remap*' + '*Imgproc_MedianBlur*' '*Imgproc_Warp*' ) IMGPROC_TEST_PATTERNS_STR="$(join_strings_with_colon "${IMGPROC_TEST_PATTERNS[*]}")" diff --git a/test/api/test_median_blur.cpp b/test/api/test_median_blur.cpp new file mode 100644 index 0000000000000000000000000000000000000000..60469ab2aa22f7670e0dee54de4498299f8872bd --- /dev/null +++ b/test/api/test_median_blur.cpp @@ -0,0 +1,349 @@ +// SPDX-FileCopyrightText: 2023 - 2025 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include + +#include "framework/array.h" +#include "framework/generator.h" +#include "kleidicv/kleidicv.h" +#include "test_config.h" + +#define KLEIDICV_MEDIAN_BLUR(type, type_suffix) \ + KLEIDICV_API(median_blur, kleidicv_median_blur_##type_suffix, type) + +KLEIDICV_MEDIAN_BLUR(int8_t, s8); +KLEIDICV_MEDIAN_BLUR(uint8_t, u8); +KLEIDICV_MEDIAN_BLUR(uint16_t, u16); +KLEIDICV_MEDIAN_BLUR(int16_t, s16); +KLEIDICV_MEDIAN_BLUR(uint32_t, u32); +KLEIDICV_MEDIAN_BLUR(int32_t, s32); +KLEIDICV_MEDIAN_BLUR(float, f32); + +template +class MedianBlurTest : public testing::Test { + public: + struct TestParams { + size_t width; + size_t src_padding; + size_t dst_padding; + size_t height; + size_t channels; + size_t filter_size; + kleidicv_border_type_t border_type; + }; + + static std::vector generate_test_cases( + const std::vector& widths, + const std::vector& src_paddings, + const std::vector& dst_paddings, + const std::vector& heights, const std::vector& channels, + const std::vector& filter_sizes, + const std::vector& border_types) { + std::vector cases; + + for (size_t w : widths) { + for (size_t src_pad : src_paddings) { + for (size_t dst_pad : dst_paddings) { + for (size_t h : heights) { + for (size_t c : channels) { + for (size_t f : filter_sizes) { + for (auto b : border_types) { + cases.push_back({w, src_pad, dst_pad, h, c, f, b}); + } + } + } + } + } + } + } + + return cases; + } + + static std::vector get_unpadded_test_cases() { + std::vector widths = {50}; + std::vector src_paddings = {0}; + std::vector dst_paddings = {0}; + std::vector heights = {30}; + std::vector channels = {1, 4}; + std::vector filter_sizes = {5}; + std::vector border_types = { + KLEIDICV_BORDER_TYPE_REPLICATE, KLEIDICV_BORDER_TYPE_REFLECT, + KLEIDICV_BORDER_TYPE_WRAP, KLEIDICV_BORDER_TYPE_REVERSE}; + + return generate_test_cases(widths, src_paddings, dst_paddings, heights, + channels, filter_sizes, border_types); + } + + static std::vector get_padded_test_Cases() { + std::vector widths = {20}; + std::vector src_paddings = {5}; + std::vector dst_paddings = {13}; + std::vector heights = {10}; + std::vector channels = {1, 4}; + std::vector filter_sizes = {5}; + std::vector border_types = { + KLEIDICV_BORDER_TYPE_REPLICATE}; + + return generate_test_cases(widths, src_paddings, dst_paddings, heights, + channels, filter_sizes, border_types); + } + + static std::vector get_small_image_test_Cases() { + std::vector widths = {20}; + std::vector src_paddings = {0}; + std::vector dst_paddings = {0}; + std::vector heights = {5}; + std::vector channels = {1}; + std::vector filter_sizes = {5}; + std::vector border_types = { + KLEIDICV_BORDER_TYPE_REPLICATE, KLEIDICV_BORDER_TYPE_REFLECT, + KLEIDICV_BORDER_TYPE_WRAP, KLEIDICV_BORDER_TYPE_REVERSE}; + + return generate_test_cases(widths, src_paddings, dst_paddings, heights, + channels, filter_sizes, border_types); + } + + void run_test_case(const TestParams& params) { + test::Array2D src{params.width * params.channels, + params.height, params.src_padding, + params.channels}; + test::Array2D expected_dst{params.width * params.channels, + params.height, 0, params.channels}; + test::Array2D dst{params.width * params.channels, + params.height, params.dst_padding, + params.channels}; + + test::PseudoRandomNumberGenerator input_value_random_range; + src.fill(input_value_random_range); + + calculate_reference(src, expected_dst, params.filter_size, + params.border_type); + + auto status = median_blur()( + src.data(), src.stride(), dst.data(), dst.stride(), params.width, + params.height, params.channels, params.filter_size, params.filter_size, + params.border_type); + + EXPECT_EQ(KLEIDICV_OK, status); + EXPECT_EQ_ARRAY2D(expected_dst, dst); + } + + private: + int handle_under_over_read(int index, int limit, + kleidicv_border_type_t border_type) { + int result = 0; + + if (index >= 0 && index < limit) { + result = index; + } else { + switch (border_type) { + case KLEIDICV_BORDER_TYPE_REPLICATE: { + result = std::clamp(index, 0, limit - 1); + break; + } + case KLEIDICV_BORDER_TYPE_REFLECT: { + if (index < 0) { + result = -index - 1; + } else { + result = 2 * limit - index - 1; + } + break; + } + + case KLEIDICV_BORDER_TYPE_WRAP: { + result = (index + limit) % limit; + break; + } + + case KLEIDICV_BORDER_TYPE_REVERSE: { + int period = 2 * limit - 2; + if (index < 0) { + result = (-index) % period; + } else { + result = period - (index % period); + } + break; + } + + default: + result = 0; + break; + } + } + return result; + } + + void calculate_reference(const test::Array2D& src, + test::Array2D& dst, size_t filter_size, + kleidicv_border_type_t border_type) { + const int half_kernel_size = static_cast(filter_size) / 2; + const size_t height = src.height(); + const size_t width = src.width() / src.channels(); + + for (size_t row = 0; row < height; ++row) { + for (size_t col = 0; col < width; ++col) { + for (size_t channel = 0; channel < src.channels(); ++channel) { + std::vector window; + + for (int window_row = -half_kernel_size; + window_row <= half_kernel_size; ++window_row) { + for (int window_col = -half_kernel_size; + window_col <= half_kernel_size; ++window_col) { + int row_after_border_handling = + handle_under_over_read(row + window_row, height, border_type); + int col_after_border_handling = + handle_under_over_read(col + window_col, width, border_type); + + window.push_back(*src.at( + row_after_border_handling, + col_after_border_handling * src.channels() + channel)); + } + } + + std::sort(window.begin(), window.end()); + dst.set(row, col * dst.channels() + channel, + {window[window.size() / 2]}); + } + } + } + } +}; + +using ElementTypes = ::testing::Types; +TYPED_TEST_SUITE(MedianBlurTest, ElementTypes); + +TYPED_TEST(MedianBlurTest, RunAllParamCombinationsWithoutPadding) { + if (test::Options::are_long_running_tests_skipped()) { + GTEST_SKIP() << "Long running exp test skipped"; + } + + for (const auto& params : TestFixture::get_unpadded_test_cases()) { + this->run_test_case(params); + } +} + +TYPED_TEST(MedianBlurTest, RunAllParamCombinationsWithSmallImageSize) { + for (const auto& params : TestFixture::get_small_image_test_Cases()) { + this->run_test_case(params); + } +} + +TYPED_TEST(MedianBlurTest, ChannelNumber) { + test::Array2D src{25, 25, 0, KLEIDICV_MAXIMUM_CHANNEL_COUNT + 1}; + test::Array2D dst{25, 25, 0, KLEIDICV_MAXIMUM_CHANNEL_COUNT + 1}; + EXPECT_EQ(KLEIDICV_ERROR_NOT_IMPLEMENTED, + median_blur()(src.data(), src.stride(), dst.data(), + dst.stride(), 25, 25, + KLEIDICV_MAXIMUM_CHANNEL_COUNT + 1, 5, 5, + KLEIDICV_BORDER_TYPE_REPLICATE)); +} + +TYPED_TEST(MedianBlurTest, BorderNotImplemented) { + test::Array2D src{25, 25}; + test::Array2D dst{25, 25}; + auto status = median_blur()(src.data(), src.stride(), dst.data(), + dst.stride(), 25, 25, 1, 5, 5, + KLEIDICV_BORDER_TYPE_TRANSPARENT); + EXPECT_EQ(KLEIDICV_ERROR_NOT_IMPLEMENTED, status); +} + +TYPED_TEST(MedianBlurTest, HeightTooSmall) { + test::Array2D src{100, 3}; + test::Array2D dst{100, 3}; + EXPECT_EQ(KLEIDICV_ERROR_NOT_IMPLEMENTED, + median_blur()(src.data(), src.stride(), dst.data(), + dst.stride(), 100, 3, 1, 5, 5, + KLEIDICV_BORDER_TYPE_REPLICATE)); +} + +TYPED_TEST(MedianBlurTest, WidthTooSmall) { + test::Array2D src{3, 100}; + test::Array2D dst{3, 100}; + EXPECT_EQ(KLEIDICV_ERROR_NOT_IMPLEMENTED, + median_blur()(src.data(), src.stride(), dst.data(), + dst.stride(), 3, 100, 1, 5, 5, + KLEIDICV_BORDER_TYPE_REPLICATE)); +} + +TYPED_TEST(MedianBlurTest, NullPointer) { + test::Array2D src{5, 5}; + test::Array2D dst{5, 5}; + test::test_null_args(median_blur(), src.data(), src.stride(), + dst.data(), dst.stride(), 5, 5, 1, 5, 5, + KLEIDICV_BORDER_TYPE_REPLICATE); +} + +TYPED_TEST(MedianBlurTest, OversizeImage) { + test::Array2D src{5, 5}; + test::Array2D dst{5, 5}; + EXPECT_EQ(KLEIDICV_ERROR_RANGE, + median_blur()(src.data(), src.stride(), dst.data(), + dst.stride(), KLEIDICV_MAX_IMAGE_PIXELS, + KLEIDICV_MAX_IMAGE_PIXELS, 1, 5, 5, + KLEIDICV_BORDER_TYPE_REPLICATE)); + EXPECT_EQ(KLEIDICV_ERROR_RANGE, + median_blur()( + src.data(), src.stride(), dst.data(), dst.stride(), + KLEIDICV_MAX_IMAGE_PIXELS + 1, KLEIDICV_MAX_IMAGE_PIXELS, 1, 5, + 5, KLEIDICV_BORDER_TYPE_REPLICATE)); + EXPECT_EQ(KLEIDICV_ERROR_RANGE, + median_blur()(src.data(), src.stride(), dst.data(), + dst.stride(), KLEIDICV_MAX_IMAGE_PIXELS, + KLEIDICV_MAX_IMAGE_PIXELS + 1, 1, 5, 5, + KLEIDICV_BORDER_TYPE_REPLICATE)); + EXPECT_EQ( + KLEIDICV_ERROR_RANGE, + median_blur()(src.data(), src.stride(), dst.data(), + dst.stride(), 10, KLEIDICV_MAX_IMAGE_PIXELS, 1, + 5, 5, KLEIDICV_BORDER_TYPE_REPLICATE)); +} + +TYPED_TEST(MedianBlurTest, UnsupportedLargeFilterSize) { + test::Array2D src{100, 100}; + test::Array2D dst{100, 100}; + EXPECT_EQ(KLEIDICV_ERROR_NOT_IMPLEMENTED, + median_blur()(src.data(), src.stride(), dst.data(), + dst.stride(), 100, 100, 1, 100, 100, + KLEIDICV_BORDER_TYPE_REPLICATE)); +} + +TYPED_TEST(MedianBlurTest, NonSquareFilterSizeWithValidHeight) { + test::Array2D src{100, 100}; + test::Array2D dst{100, 100}; + EXPECT_EQ(KLEIDICV_ERROR_NOT_IMPLEMENTED, + median_blur()(src.data(), src.stride(), dst.data(), + dst.stride(), 100, 100, 1, 100, 5, + KLEIDICV_BORDER_TYPE_REPLICATE)); +} + +TYPED_TEST(MedianBlurTest, NonSquareFilterSizeWithValidWidth) { + test::Array2D src{100, 100}; + test::Array2D dst{100, 100}; + EXPECT_EQ(KLEIDICV_ERROR_NOT_IMPLEMENTED, + median_blur()(src.data(), src.stride(), dst.data(), + dst.stride(), 100, 100, 1, 5, 100, + KLEIDICV_BORDER_TYPE_REPLICATE)); +} + +template +class MedianBlurByteStrideTest : public MedianBlurTest {}; + +using ByteStrideTypes = ::testing::Types; +TYPED_TEST_SUITE(MedianBlurByteStrideTest, ByteStrideTypes); + +TYPED_TEST(MedianBlurByteStrideTest, RunAllParamCombinationsWithPadding) { + if (test::Options::are_long_running_tests_skipped()) { + GTEST_SKIP() << "Long running exp test skipped"; + } + + for (const auto& params : TestFixture::get_padded_test_Cases()) { + this->run_test_case(params); + } +} diff --git a/test/api/test_thread.cpp b/test/api/test_thread.cpp index 876768095275d770ccb47d35dcf51a246d0ad7bb..3829b04bdad0b9f7a82060406b28bc2619fe11cc 100644 --- a/test/api/test_thread.cpp +++ b/test/api/test_thread.cpp @@ -106,6 +106,18 @@ class Thread : public testing::TestWithParam

{ ASSERT_EQ(KLEIDICV_OK, kleidicv_filter_context_release(context)); } + template + void check_median_blur(SingleThreadedFunc single_threaded_func, + MultithreadedFunc multithreaded_func) { + auto [width, height, thread_count] = GetParam(); + (void)thread_count; + size_t channels = 1; + size_t ksize = 5; + kleidicv_border_type_t border_type = KLEIDICV_BORDER_TYPE_REPLICATE; + check_unary_op(single_threaded_func, multithreaded_func, channels, + channels, channels, ksize, ksize, border_type); + } + template void check_remap_s16(SingleThreadedFunc single_threaded_func, @@ -355,6 +367,80 @@ TEST_P(Thread, gaussian_blur_u8) { ASSERT_EQ(KLEIDICV_OK, kleidicv_filter_context_release(context)); } +template +void check_median_blur_not_implemented(MultithreadedFunc multithreaded_func) { + constexpr size_t channels = 1; + constexpr size_t kernel_width = 1; + constexpr size_t kernel_height = kernel_width; + test::Array2D src(1, 1); + test::Array2D dst(1, 1); + EXPECT_EQ(KLEIDICV_ERROR_NOT_IMPLEMENTED, + multithreaded_func(src.data(), src.stride(), dst.data(), + dst.stride(), 1, 1, channels, kernel_width, + kernel_height, KLEIDICV_BORDER_TYPE_REPLICATE, + get_multithreading_fake(2))); + + constexpr size_t max_width = 25, max_height = 25; + test::Array2D src1(max_width, max_height); + test::Array2D dst1(max_width, max_height); + EXPECT_EQ(KLEIDICV_ERROR_NOT_IMPLEMENTED, + multithreaded_func( + src1.data(), src1.stride(), dst1.data(), dst1.stride(), + max_width, max_height, channels, kernel_width, kernel_height, + KLEIDICV_BORDER_TYPE_TRANSPARENT, get_multithreading_fake(2))); + + EXPECT_EQ( + KLEIDICV_ERROR_NOT_IMPLEMENTED, + multithreaded_func(src1.data(), src1.stride(), src1.data(), src1.stride(), + 25, 25, 1, 5, 5, KLEIDICV_BORDER_TYPE_REPLICATE, + get_multithreading_fake(2))); + + EXPECT_EQ(KLEIDICV_ERROR_NOT_IMPLEMENTED, + multithreaded_func( + src1.data(), src1.stride(), dst1.data(), dst1.stride(), 25, 25, + KLEIDICV_MAXIMUM_CHANNEL_COUNT + 1, 5, 5, + KLEIDICV_BORDER_TYPE_REPLICATE, get_multithreading_fake(2))); + + EXPECT_EQ( + KLEIDICV_ERROR_NOT_IMPLEMENTED, + multithreaded_func(src1.data(), src1.stride(), dst1.data(), dst1.stride(), + 25, 25, 1, 5, 3, KLEIDICV_BORDER_TYPE_REPLICATE, + get_multithreading_fake(2))); + + EXPECT_EQ( + KLEIDICV_ERROR_NOT_IMPLEMENTED, + multithreaded_func(src1.data(), src1.stride(), dst1.data(), dst1.stride(), + 25, 25, 1, 3, 3, KLEIDICV_BORDER_TYPE_REPLICATE, + get_multithreading_fake(2))); +} + +TEST(ThreadMedianBlur, NotImplemented) { + check_median_blur_not_implemented(kleidicv_thread_median_blur_u8); + check_median_blur_not_implemented(kleidicv_thread_median_blur_u16); + check_median_blur_not_implemented(kleidicv_thread_median_blur_s16); + check_median_blur_not_implemented(kleidicv_thread_median_blur_f32); +} + +TEST_P(Thread, median_blur_u8) { + check_median_blur(kleidicv_median_blur_u8, + kleidicv_thread_median_blur_u8); +} + +TEST_P(Thread, median_blur_s16) { + check_median_blur(kleidicv_median_blur_s16, + kleidicv_thread_median_blur_s16); +} + +TEST_P(Thread, median_blur_u16) { + check_median_blur(kleidicv_median_blur_u16, + kleidicv_thread_median_blur_u16); +} + +TEST_P(Thread, median_blur_f32) { + check_median_blur(kleidicv_median_blur_f32, + kleidicv_thread_median_blur_f32); +} + TEST(ThreadGaussianBlur, NotImplemented) { unsigned max_width = 10, max_height = 10; size_t channels = 1;