From 92774514d58a79630107cbee312828903ec8a572 Mon Sep 17 00:00:00 2001 From: Maksims Svecovs Date: Wed, 21 Feb 2024 00:39:15 +0000 Subject: [PATCH 1/2] [test] add canny test Signed-off-by: Maksims Svecovs --- test/api/test_canny.cpp | 252 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 249 insertions(+), 3 deletions(-) diff --git a/test/api/test_canny.cpp b/test/api/test_canny.cpp index 4be2534d6..2859963ff 100644 --- a/test/api/test_canny.cpp +++ b/test/api/test_canny.cpp @@ -4,8 +4,240 @@ #include +#include +#include + +#include "framework/array.h" +#include "framework/generator.h" #include "framework/utils.h" #include "intrinsiccv/intrinsiccv.h" +#include "intrinsiccv/traits.h" + +template +struct CannyTestParams; + +template <> +struct CannyTestParams { + using ElementType = uint8_t; + using IntermediateType = int16_t; +}; + +template +class CannyTestSuite { + public: + void test() { + low_threshold_ = 0x200; + high_threshold_ = 0x380; + constexpr size_t width = 200; + constexpr size_t height = 200; + + test::Array2D source(width, height); + test::Array2D actual(width, height); + test::Array2D expected(width, height); + + test::PseudoRandomNumberGenerator generator; + source.fill(generator); + + // Make sure there is some edge in generated source. + source.set(0, 0, {200, 200, 0, 0, 0}); + source.set(1, 0, {200, 200, 0, 0, 0}); + source.set(2, 0, {200, 0, 0, 200, 200}); + source.set(3, 0, {0, 0, 0, 200, 200}); + source.set(4, 0, {0, 0, 0, 200, 200}); + + calculate_expected(source, expected); + + auto err = intrinsiccv_canny_u8(source.data(), source.stride(), + actual.data(), actual.stride(), width, + height, low_threshold_, high_threshold_); + ASSERT_EQ(INTRINSICCV_OK, err); + + EXPECT_EQ_ARRAY2D(actual, expected); + } + + private: + using ElementType = typename TestParams::ElementType; + using IntermediateType = typename TestParams::IntermediateType; + + struct Coords { + size_t row; + size_t column; + }; + + void calculate_expected(test::Array2D &src, + test::Array2D &dst) { + // Allocate temp buffers for gradients + size_t width = dst.width(); + size_t height = dst.height(); + test::Array2D horizontal_gradient(width, height); + test::Array2D vertical_gradient(width, height); + test::Array2D magnitudes(width, height); + + // Calculate horizontal gradient + auto err = intrinsiccv_sobel_3x3_horizontal_s16_u8( + src.data(), src.stride(), horizontal_gradient.data(), + horizontal_gradient.stride(), width, height, 1); + ASSERT_EQ(INTRINSICCV_OK, err); + + // Calculate vertical gradient + err = intrinsiccv_sobel_3x3_vertical_s16_u8( + src.data(), src.stride(), vertical_gradient.data(), + vertical_gradient.stride(), width, height, 1); + ASSERT_EQ(INTRINSICCV_OK, err); + + // Calculate magnitude from the horizontal and vertical derivatives, and + // apply lower threshold. + err = intrinsiccv_saturating_add_abs_with_threshold_s16( + horizontal_gradient.data(), horizontal_gradient.stride(), + vertical_gradient.data(), vertical_gradient.stride(), magnitudes.data(), + magnitudes.stride(), width, height, low_threshold_); + ASSERT_EQ(INTRINSICCV_OK, err); + + // Edge thining + non_maxima_suppression(horizontal_gradient, vertical_gradient, magnitudes); + + // Promote weak pixels + perform_hysteresis_supress_weak_edges(magnitudes, dst); + } + + void promote_weak(test::Array2D &source, + std::vector &strongPixels) { + // Walk through all strong pixels including added in this function + while (!strongPixels.empty()) { + Coords current = strongPixels.back(); + strongPixels.pop_back(); + + // Promote weak pixels in 3x3 window around current + for (int8_t y = -1; y < 2; ++y) { + for (int8_t x = -1; x < 2; ++x) { + if (current.column + x >= 0 && + current.column + x < static_cast<__int128_t>(source.width()) && + current.row + y >= 0 && + current.row + y < static_cast<__int128_t>(source.height())) { + if (x == y && y == 0) { + continue; + } + if (source.at(current.row + y, current.column + x)[0] > 0 && + source.at(current.row + y, current.column + x)[0] <= + high_threshold_) { + size_t strong_row = current.row + y; + size_t strong_col = current.column + x; + source.at(strong_row, strong_col)[0] = high_threshold_ + 1; + strongPixels.push_back({strong_row, strong_col}); + } + } + } + } + } + } + + void perform_hysteresis_supress_weak_edges( + test::Array2D &source, + test::Array2D &dst) { + std::vector strongPixels; + + // Find all strong pixels + for (size_t row = 0; row < source.height(); ++row) { + for (size_t column = 0; column < source.width(); ++column) { + if (source.at(row, column)[0] > high_threshold_) { + strongPixels.push_back({row, column}); + } + } + } + + // Promote weak edges + promote_weak(source, strongPixels); + + // Fill destination + for (size_t row = 0; row < dst.height(); ++row) { + for (size_t column = 0; column < dst.width(); ++column) { + if (source.at(row, column)[0] > high_threshold_) { + dst.at(row, column)[0] = 0xff; + } + } + } + } + + void non_maxima_suppression(test::Array2D &Gx, + test::Array2D &Gy, + test::Array2D &magnitudes) { + constexpr double c_pi = 3.14159265358979323846; + test::Array2D temp = magnitudes; + size_t row = 0; + size_t column = 0; + + auto drop_weak_mag = [&](IntermediateType prev, IntermediateType next) { + if (temp.at(row, column)[0] <= prev || temp.at(row, column)[0] < next) { + magnitudes.at(row, column)[0] = 0; + } + }; + + for (row = 0; row < magnitudes.height(); ++row) { + for (column = 0; column < magnitudes.width(); ++column) { + if (temp.at(row, column)[0] == 0) { + continue; + } + + // Calculate gradient direction and confine it to [0,c_pi] + double angle = std::atan2(Gy.at(row, column)[0], Gx.at(row, column)[0]); + if (angle < 0) { + angle += c_pi; + } + + if (angle > 7 * c_pi / 8 || angle < c_pi / 8) { + // right and left + auto left_mag = column == 0 ? 0 : temp.at(row, column - 1)[0]; + auto right_mag = + column == temp.width() - 1 ? 0 : temp.at(row, column + 1)[0]; + drop_weak_mag(left_mag, right_mag); + } else if (angle > 3 * c_pi / 8 && angle < 5 * c_pi / 8) { + // top and bottom + auto top_mag = row == 0 ? 0 : temp.at(row - 1, column)[0]; + auto bottom_mag = + row == temp.height() - 1 ? 0 : temp.at(row + 1, column)[0]; + drop_weak_mag(top_mag, bottom_mag); +#if INTRINSICCV_CANNY_ALGORITHM_CONFORM_OPENCV + // top-right and bottom-left + // diagonal directions are swapped and `prev >= current < next` + // becomes `prev >= current <= next` + } else if (angle > 5 * c_pi / 8 && angle < 7 * c_pi / 8) { + auto top_right_mag = 1; + auto bottom_left_mag = 0; +#else + } else if (angle > c_pi / 8 && angle < 3 * c_pi / 8) { + auto top_right_mag = 0; + auto bottom_left_mag = 0; +#endif + top_right_mag += (column == temp.width() - 1 || row == 0) + ? 0 + : temp.at(row - 1, column + 1)[0]; + bottom_left_mag += (column == 0 || row == temp.height() - 1) + ? 0 + : temp.at(row + 1, column - 1)[0]; + drop_weak_mag(bottom_left_mag, top_right_mag); + } else { + // top-left and bottom-right +#if INTRINSICCV_CANNY_ALGORITHM_CONFORM_OPENCV + auto top_left_mag = 0; + auto bottom_right_mag = 1; +#else + auto top_left_mag = 0; + auto bottom_right_mag = 0; +#endif + top_left_mag += + (column == 0 || row == 0) ? 0 : temp.at(row - 1, column - 1)[0]; + bottom_right_mag += + (column == temp.width() - 1 || row == temp.height() - 1) + ? 0 + : temp.at(row + 1, column + 1)[0]; + drop_weak_mag(top_left_mag, bottom_right_mag); + } + } + } + } + + int16_t low_threshold_, high_threshold_; +}; // end of class CannyTestSuite #if INTRINSICCV_EXPERIMENTAL_FEATURE_CANNY @@ -27,6 +259,12 @@ TYPED_TEST(CannyTest, NullPointer) { sizeof(TypeParam), 1, 1, 0.0, 1.0); } +TYPED_TEST(CannyTest, Values) { + using KernelTestParams = CannyTestParams; + CannyTestSuite test{}; + test.test(); +} + TYPED_TEST(CannyTest, Misalignment) { if (sizeof(TypeParam) == 1) { // misalignment impossible @@ -43,16 +281,16 @@ TYPED_TEST(CannyTest, Misalignment) { TYPED_TEST(CannyTest, ZeroImageSize) { TypeParam src[1] = {}, dst[1]; - EXPECT_EQ(INTRINSICCV_OK, + EXPECT_EQ(INTRINSICCV_ERROR_NOT_IMPLEMENTED, canny()(src, sizeof(TypeParam), dst, sizeof(TypeParam), 0, 1, 0.0, 1.0)); - EXPECT_EQ(INTRINSICCV_OK, + EXPECT_EQ(INTRINSICCV_ERROR_NOT_IMPLEMENTED, canny()(src, sizeof(TypeParam), dst, sizeof(TypeParam), 1, 0, 0.0, 1.0)); } TYPED_TEST(CannyTest, OversizeImage) { - TypeParam src[1], dst[1]; + TypeParam src[1] = {}, dst[1]; EXPECT_EQ(INTRINSICCV_ERROR_RANGE, canny()(src, sizeof(TypeParam), dst, sizeof(TypeParam), INTRINSICCV_MAX_IMAGE_PIXELS + 1, 1, 0.0, 1.0)); @@ -62,4 +300,12 @@ TYPED_TEST(CannyTest, OversizeImage) { INTRINSICCV_MAX_IMAGE_PIXELS, 0.0, 1.0)); } +TYPED_TEST(CannyTest, CannotAllocateBuffers) { + MockMallocToFail::enable(); + TypeParam src[5] = {}, dst[5] = {}; + EXPECT_EQ(INTRINSICCV_ERROR_ALLOCATION, + canny()(src, sizeof(TypeParam), dst, sizeof(TypeParam), + 5, 5, 0.0, 1.0)); + MockMallocToFail::disable(); +} #endif // INTRINSICCV_EXPERIMENTAL_FEATURE_CANNY -- GitLab From 00fb2605f41e7d4030b18f9d497a1bee58e79b1d Mon Sep 17 00:00:00 2001 From: Maksims Svecovs Date: Mon, 11 Mar 2024 23:55:40 +0000 Subject: [PATCH 2/2] Remove canny from experimental Since Canny was added to experimental because of the lack of testing - removing experimental tag Signed-off-by: Maksims Svecovs --- adapters/opencv/intrinsiccv_hal.cpp | 2 -- adapters/opencv/intrinsiccv_hal.h | 4 ---- intrinsiccv/CMakeLists.txt | 1 - intrinsiccv/include/intrinsiccv/config.h.in | 2 -- intrinsiccv/include/intrinsiccv/intrinsiccv.h | 2 -- intrinsiccv/include/intrinsiccv/types.h | 2 +- intrinsiccv/src/analysis/canny_neon.cpp | 4 ---- test/api/test_canny.cpp | 3 --- 8 files changed, 1 insertion(+), 19 deletions(-) diff --git a/adapters/opencv/intrinsiccv_hal.cpp b/adapters/opencv/intrinsiccv_hal.cpp index 4e2f2af04..401b20a7d 100644 --- a/adapters/opencv/intrinsiccv_hal.cpp +++ b/adapters/opencv/intrinsiccv_hal.cpp @@ -495,7 +495,6 @@ int sobel(const uchar *src_data, size_t src_step, uchar *dst_data, return CV_HAL_ERROR_NOT_IMPLEMENTED; } -#if INTRINSICCV_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, double highThreshold, int ksize, bool L2gradient) { @@ -521,7 +520,6 @@ int canny(const uchar *src_data, size_t src_step, uchar *dst_data, static_cast(width), static_cast(height), lowThreshold, highThreshold)); } -#endif // INTRINSICCV_EXPERIMENTAL_FEATURE_CANNY int transpose(const uchar *src_data, size_t src_step, uchar *dst_data, size_t dst_step, int src_width, int src_height, diff --git a/adapters/opencv/intrinsiccv_hal.h b/adapters/opencv/intrinsiccv_hal.h index 1eab08d6e..fcbbd8b54 100644 --- a/adapters/opencv/intrinsiccv_hal.h +++ b/adapters/opencv/intrinsiccv_hal.h @@ -70,11 +70,9 @@ 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); -#if INTRINSICCV_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, double highThreshold, int ksize, bool L2gradient); -#endif // INTRINSICCV_EXPERIMENTAL_FEATURE_CANNY int transpose(const uchar *src_data, size_t src_step, uchar *dst_data, size_t dst_step, int src_width, int src_height, int element_size); @@ -226,7 +224,6 @@ static inline int intrinsiccv_sobel_with_fallback( #undef cv_hal_sobel #define cv_hal_sobel intrinsiccv_sobel_with_fallback -#if INTRINSICCV_EXPERIMENTAL_FEATURE_CANNY // canny static inline int intrinsiccv_canny_with_fallback( const uchar *src_data, size_t src_step, uchar *dst_data, size_t dst_step, @@ -238,7 +235,6 @@ static inline int intrinsiccv_canny_with_fallback( } #undef cv_hal_canny #define cv_hal_canny intrinsiccv_canny_with_fallback -#endif // INTRINSICCV_EXPERIMENTAL_FEATURE_CANNY #endif // OPENCV_IMGPROC_HAL_REPLACEMENT_HPP diff --git a/intrinsiccv/CMakeLists.txt b/intrinsiccv/CMakeLists.txt index 188b61fa3..53eb4ea32 100644 --- a/intrinsiccv/CMakeLists.txt +++ b/intrinsiccv/CMakeLists.txt @@ -14,7 +14,6 @@ option(INTRINSICCV_ENABLE_SME2 "Explicitly enables or disables SME2 code paths f option(INTRINSICCV_CHECK_BANNED_FUNCTIONS "Check source for deprecated or obsolescent functions" OFF) option(INTRINSICCV_ASSUME_128BIT_SVE2 "Internal - If turned ON 128-bit SVE2 vector length is assumed" OFF) option(INTRINSICCV_PREFER_INTERLEAVING_LOAD_STORE "Internal - If turned ON interleaving loads and stores are preferred instead of continuous loads and stores" OFF) -option(INTRINSICCV_EXPERIMENTAL_FEATURE_CANNY "Internal - Enable experimental Canny algorithm" OFF) option(INTRINSICCV_CANNY_ALGORITHM_CONFORM_OPENCV "Internal - If turned ON Canny algorithm creates bit exact result compared to OpenCV's original implementation" ON) if (INTRINSICCV_ENABLE_SVE2 AND INTRINSICCV_ENABLE_SVE2_SELECTIVELY) diff --git a/intrinsiccv/include/intrinsiccv/config.h.in b/intrinsiccv/include/intrinsiccv/config.h.in index a92ddfc1a..dd5b0378c 100644 --- a/intrinsiccv/include/intrinsiccv/config.h.in +++ b/intrinsiccv/include/intrinsiccv/config.h.in @@ -13,8 +13,6 @@ #cmakedefine01 INTRINSICCV_PREFER_INTERLEAVING_LOAD_STORE -#cmakedefine01 INTRINSICCV_EXPERIMENTAL_FEATURE_CANNY - #cmakedefine01 INTRINSICCV_CANNY_ALGORITHM_CONFORM_OPENCV // Set to '1' if compiling NEON code paths, otherwise it is set to '0'. diff --git a/intrinsiccv/include/intrinsiccv/intrinsiccv.h b/intrinsiccv/include/intrinsiccv/intrinsiccv.h index c691afc7c..ce2cb5fe7 100644 --- a/intrinsiccv/include/intrinsiccv/intrinsiccv.h +++ b/intrinsiccv/include/intrinsiccv/intrinsiccv.h @@ -915,7 +915,6 @@ intrinsiccv_error_t intrinsiccv_sobel_3x3_horizontal_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); -#if INTRINSICCV_EXPERIMENTAL_FEATURE_CANNY /// Canny edge detector for uint8_t grayscale input. Output is also a uint8_t /// grayscale image. Width and height are the same for input and output. Number /// of pixels is limited to @ref INTRINSICCV_MAX_IMAGE_PIXELS. @@ -948,7 +947,6 @@ intrinsiccv_error_t intrinsiccv_canny_u8(const uint8_t *src, size_t src_stride, size_t width, size_t height, double low_threshold, double high_threshold); -#endif // INTRINSICCV_EXPERIMENTAL_FEATURE_CANNY /// Creates a filter context according to the parameters. /// diff --git a/intrinsiccv/include/intrinsiccv/types.h b/intrinsiccv/include/intrinsiccv/types.h index 41510e109..c5e5b6d59 100644 --- a/intrinsiccv/include/intrinsiccv/types.h +++ b/intrinsiccv/include/intrinsiccv/types.h @@ -371,7 +371,7 @@ class Rows final : public RowBase { } // Pointer to the first row. - T *ptr_; + T *ptr_ = nullptr; }; // end of class Rows // Similar to Rows, but in this case rows are indirectly addressed. diff --git a/intrinsiccv/src/analysis/canny_neon.cpp b/intrinsiccv/src/analysis/canny_neon.cpp index 67fc5e045..d412c7fdb 100644 --- a/intrinsiccv/src/analysis/canny_neon.cpp +++ b/intrinsiccv/src/analysis/canny_neon.cpp @@ -6,8 +6,6 @@ #include "intrinsiccv/intrinsiccv.h" #include "intrinsiccv/neon.h" -#if INTRINSICCV_EXPERIMENTAL_FEATURE_CANNY - namespace intrinsiccv::neon { // Container to hold strong edges. @@ -545,5 +543,3 @@ extern "C" INTRINSICCV_TARGET_FN_ATTRS intrinsiccv_error_t intrinsiccv_canny_u8( } } // namespace intrinsiccv::neon - -#endif // INTRINSICCV_EXPERIMENTAL_FEATURE_CANNY diff --git a/test/api/test_canny.cpp b/test/api/test_canny.cpp index 2859963ff..713ccdb37 100644 --- a/test/api/test_canny.cpp +++ b/test/api/test_canny.cpp @@ -239,8 +239,6 @@ class CannyTestSuite { int16_t low_threshold_, high_threshold_; }; // end of class CannyTestSuite -#if INTRINSICCV_EXPERIMENTAL_FEATURE_CANNY - #define INTRINSICCV_CANNY(type, suffix) \ INTRINSICCV_API(canny, intrinsiccv_canny_##suffix, type) @@ -308,4 +306,3 @@ TYPED_TEST(CannyTest, CannotAllocateBuffers) { 5, 5, 0.0, 1.0)); MockMallocToFail::disable(); } -#endif // INTRINSICCV_EXPERIMENTAL_FEATURE_CANNY -- GitLab