From 5cf77a94222165456d040e072866dd4c6ad012e0 Mon Sep 17 00:00:00 2001 From: Gin <> Date: Wed, 21 Oct 2026 20:21:07 -0700 Subject: [PATCH 1/3] Test every range filter and filter_by_mask() implementation on real images The old ImageFilter tests were debug code: they were not registered, ran 3-second timing loops, and only ran the implementation the CPU dispatch picks. Now each range filter and filter_by_mask() implementation that the build has and the CPU supports gets a test on 4 CommandLineTests images, checked against a scalar reference. The two Euclidean tests are removed. Kernels::RGB32Euclidean covers them. Co-Authored-By: Claude Opus 5.5 --- .../Kernels_ImageFilter_Tests.cpp | 858 +++++++----------- 1 file changed, 328 insertions(+), 530 deletions(-) diff --git a/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp b/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp index 6757ab2420..511c7b172d 100644 --- a/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp +++ b/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp @@ -4,608 +4,406 @@ * */ -#include "Common/Cpp/Color.h" -#include "Common/Cpp/Time.h" +#include +#include +#include "Common/Cpp/CancellableScope.h" +#include "Common/Cpp/CpuId/CpuId.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "CommonFramework/GlobalAutoPaths.h" -#include "CommonFramework/ImageTypes/BinaryImage.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "CommonFramework/ImageTypes/ImageViewRGB32.h" #include "Kernels/BinaryMatrix/Kernels_BinaryMatrix.h" -#ifdef PA_AutoDispatch_arm64_20_M1 -#include "Kernels/BinaryMatrix/Kernels_BinaryMatrixTile_64x8_arm64_NEON.h" -#include "Kernels/PartialWordAccess/Kernels_PartialWordAccess_arm64_NEON.h" -#include "Kernels/Waterfill/Kernels_Waterfill_Core_64x8_arm64_NEON.h" -#endif -//#include "Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64xH_Default.h" -#include "Kernels/BinaryImageFilters/Kernels_BinaryImage_BasicFilters.h" -#include "Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.h" -#include "Kernels/ImageFilters/RGB32_EuclideanDistance/Kernels_ImageFilter_RGB32_Euclidean.h" -#include "Kernels_ImageFilter_Tests.h" #include "Tests/GuardPageImage.h" -#include "Tests/TestUtils.h" +#include "Kernels_ImageFilter_Tests.h" -#include -using std::cout; -using std::cerr; -using std::endl; -using std::flush; namespace PokemonAutomation{ namespace Kernels{ -class Test_ImageFilterRGB32Range : public UnitTest{ -public: - Test_ImageFilterRGB32Range( - const std::string& image - ) - : UnitTest("Kernels::ImageFilterRGB32Range - " + image) - , m_image(UNIT_TEST_RESOURCE_PATH() + image) - {} - - virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ - // Copy the test image into guard-page memory with packed rows, so a - // kernel that reads or writes past the last pixel faults. - ImageRGB32 file_image(m_image); - GuardPageImageRGB32 image(file_image); - - const size_t width = image.width(), height = image.height(); - cout << "Testing filter_rgb32_range(), image size " << width << " x " << height << endl; - - Color min_color(0, 0, 0); - Color max_color(63, 63, 63); - // Color max_color(238, 24, 42); - - const uint32_t mins = uint32_t(min_color); - const uint32_t maxs = uint32_t(max_color); - - GuardPageImageRGB32 image_out(image.width(), image.height()); - GuardPageImageRGB32 image_out_2(image.width(), image.height()); - size_t pixels_in_range = 0; - - const bool replace_color_within_range = true; - auto time_start = current_time(); - // auto new_image = filter_rgb32_range(image, mins, maxs, COLOR_WHITE, replace_color_within_range); - pixels_in_range = Kernels::filter_rgb32_range( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out.data(), image_out.bytes_per_row(), - (uint32_t)COLOR_WHITE, replace_color_within_range, - mins, maxs - ); - auto time_end = current_time(); - auto ns = std::chrono::duration_cast(time_end - time_start).count(); - auto ms = ns / 1000000.; - cout << "One filter time: " << ms << " ms" << endl; - - size_t pixels_in_range_2 = Kernels::filter_rgb32_range( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out_2.data(), image_out_2.bytes_per_row(), - (uint32_t)COLOR_WHITE, !replace_color_within_range, - mins, maxs - ); - - TEST_RESULT_EQUAL_STR(pixels_in_range, pixels_in_range_2); - - size_t actual_num_pixels_in_range = 0; - size_t error_count = 0; - for (size_t y = 0; y < height; y++){ - for (size_t x = 0; x < width; x++){ - const Color color(image.pixel(x, y)); - const Color new_color(image_out.pixel(x, y)); - const Color new_color_2(image_out_2.pixel(x, y)); - bool in_range = (min_color.alpha() <= color.alpha() && color.alpha() <= max_color.alpha()); - in_range = in_range && (min_color.red() <= color.red() && color.red() <= max_color.red()); - in_range = in_range && (min_color.green() <= color.green() && color.green() <= max_color.green()); - in_range = in_range && (min_color.blue() <= color.blue() && color.blue() <= max_color.blue()); - actual_num_pixels_in_range += in_range; - if (error_count < 10){ - // Print first 10 errors: - if (in_range && new_color != COLOR_WHITE){ - cout << "Error: wrong filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should be in range but not found by the function" << endl; - ++error_count; - }else if (in_range == false && new_color != color){ - cout << "Error: wrong filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should not be changed by the function" << endl; - ++error_count; - } - - if (in_range && new_color_2 != color){ - cout << "Error: wrong inverse filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should not be changed by the function" << endl; - ++error_count; - }else if (in_range == false && new_color_2 != COLOR_WHITE){ - cout << "Error: wrong inverse filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should not be in range but not found by the function" << endl; - ++error_count; - } - } - } - } - cout << "Found " << actual_num_pixels_in_range << " pixels in range" << endl; - if (pixels_in_range != actual_num_pixels_in_range){ - cout << "Error: wrong pixels in range: " << pixels_in_range << " actual: " << actual_num_pixels_in_range << endl; - return false; - } - if (error_count){ - return false; - } +// The range filter implementations, defined in RGB32_Range/Kernels_ImageFilter_RGB32_Range_*.cpp. +// `filter_rgb32_range()` and `to_blackwhite_rgb32_range()` in Kernels_ImageFilter_RGB32_Range.cpp +// declare the same functions and call the best one for the CPU. These tests call them directly, +// so that every implementation this CPU supports is tested. +size_t filter_rgb32_range_Default( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + uint32_t replacement, bool replace_color_within_range, + uint32_t mins, uint32_t maxs +); +size_t to_blackwhite_rgb32_range_Default( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + bool in_range_black, + uint32_t mins, uint32_t maxs +); +size_t filter_rgb32_range_x64_SSE42( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + uint32_t replacement, bool replace_color_within_range, + uint32_t mins, uint32_t maxs +); +size_t to_blackwhite_rgb32_range_x64_SSE42( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + bool in_range_black, + uint32_t mins, uint32_t maxs +); +size_t filter_rgb32_range_x64_AVX2( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + uint32_t replacement, bool replace_color_within_range, + uint32_t mins, uint32_t maxs +); +size_t to_blackwhite_rgb32_range_x64_AVX2( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + bool in_range_black, + uint32_t mins, uint32_t maxs +); +size_t filter_rgb32_range_x64_AVX512( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + uint32_t replacement, bool replace_color_within_range, + uint32_t mins, uint32_t maxs +); +size_t to_blackwhite_rgb32_range_x64_AVX512( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + bool in_range_black, + uint32_t mins, uint32_t maxs +); +size_t filter_rgb32_range_arm64_NEON( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + uint32_t replacement, bool replace_color_within_range, + uint32_t mins, uint32_t maxs +); +size_t to_blackwhite_rgb32_range_arm64_NEON( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + bool in_range_black, + uint32_t mins, uint32_t maxs +); + +// The filter_by_mask() implementations, one per binary matrix type, defined in +// BinaryImageFilters/Kernels_BinaryImage_BasicFilters_Core_*.cpp. `filter_by_mask()` in +// Kernels_BinaryImage_BasicFilters.cpp declares the same functions and calls the one for +// the matrix type. +void filter_by_mask_64x4_Default (const PackedBinaryMatrix_IB& matrix, uint32_t* image, size_t bytes_per_row, uint32_t replace_with, bool replace_if_zero); +void filter_by_mask_64x8_x64_SSE42 (const PackedBinaryMatrix_IB& matrix, uint32_t* image, size_t bytes_per_row, uint32_t replace_with, bool replace_if_zero); +void filter_by_mask_64x16_x64_AVX2 (const PackedBinaryMatrix_IB& matrix, uint32_t* image, size_t bytes_per_row, uint32_t replace_with, bool replace_if_zero); +void filter_by_mask_64x32_x64_AVX512 (const PackedBinaryMatrix_IB& matrix, uint32_t* image, size_t bytes_per_row, uint32_t replace_with, bool replace_if_zero); +void filter_by_mask_64x64_x64_AVX512 (const PackedBinaryMatrix_IB& matrix, uint32_t* image, size_t bytes_per_row, uint32_t replace_with, bool replace_if_zero); +void filter_by_mask_64x8_arm64_NEON (const PackedBinaryMatrix_IB& matrix, uint32_t* image, size_t bytes_per_row, uint32_t replace_with, bool replace_if_zero); + + +namespace{ + +// One range filter implementation to test: its name in the test name, and its two functions. +struct RangeFilterImplementation{ + using Filter = size_t (*)( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + uint32_t replacement, bool replace_color_within_range, + uint32_t mins, uint32_t maxs + ); + using ToBlackWhite = size_t (*)( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + bool in_range_black, + uint32_t mins, uint32_t maxs + ); + + std::string name; + Filter filter; + ToBlackWhite to_blackwhite; +}; - // We try to wait for three seconds: - const size_t num_iters = size_t(3000 / ms); - time_start = current_time(); - for (size_t i = 0; i < num_iters; i++){ - Kernels::filter_rgb32_range( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out.data(), image_out.bytes_per_row(), - (uint32_t)COLOR_WHITE, replace_color_within_range, - mins, maxs - ); - } - time_end = current_time(); - ms = (double)std::chrono::duration_cast(time_end - time_start).count(); - cout << "Running " << num_iters << " iters, avg filter time: " << ms / num_iters << " ms" << endl; +// One filter_by_mask() implementation to test: its name in the test name, the binary +// matrix type it works on, and the function. +struct FilterByMaskImplementation{ + using Function = void (*)( + const PackedBinaryMatrix_IB& matrix, + uint32_t* image, size_t bytes_per_row, + uint32_t replace_with, bool replace_if_zero + ); + + std::string name; + BinaryMatrixType matrix_type; + Function filter_by_mask; +}; - return true; - }; -private: - std::string m_image; +// An ARGB color range: a pixel is in range if each of its A, R, G and B bytes is within +// the bytes of `mins` and `maxs`. +struct ColorRange{ + uint32_t mins; + uint32_t maxs; }; +// The ranges to test. Each has pixels both in and out of range on every test image. The +// bounds include 0x00, 0x7f, 0x80 and 0xff, the edges of signed and unsigned bytes. +constexpr ColorRange RANGES[] = { + {0xff000000, 0xff8080ff}, + {0xff204060, 0xffe0c0ff}, + {0xff60607f, 0xffc8c8c8}, + {0xffa0a0a0, 0xffffffff}, +}; -class Test_ImageFilterRGB32Euclidean : public UnitTest{ -public: - Test_ImageFilterRGB32Euclidean( - const std::string& image - ) - : UnitTest("Kernels::ImageFilterRGB32Euclidean - " + image) - , m_image(UNIT_TEST_RESOURCE_PATH() + image) - {} +constexpr uint32_t REPLACEMENT_COLOR = 0x13572468; +constexpr uint32_t BLACK = 0xff000000; +constexpr uint32_t WHITE = 0xffffffff; - virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ - // Copy the test image into guard-page memory with packed rows, so a - // kernel that reads or writes past the last pixel faults. - ImageRGB32 file_image(m_image); - GuardPageImageRGB32 image(file_image); - - const size_t width = image.width(); - const size_t height = image.height(); - cout << "Testing test_kernels_FilterRGB32Euclidean(), image size " << width << " x " << height << endl; - - Color middle_color = Color(image.pixel(width / 2, height / 2)); - cout << "Expected color: " << middle_color.to_string() << endl; - - double max_dist = 50.0; - size_t max_dist2 = size_t(max_dist * max_dist); - - GuardPageImageRGB32 image_out(image.width(), image.height()); - GuardPageImageRGB32 image_out_2(image.width(), image.height()); - size_t pixels_in_range = 0; - - const bool replace_color_within_range = true; - auto time_start = current_time(); - pixels_in_range = Kernels::filter_rgb32_euclidean( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out.data(), image_out.bytes_per_row(), - (uint32_t)COLOR_WHITE, replace_color_within_range, - uint32_t(middle_color), max_dist - ); - auto time_end = current_time(); - auto ns = std::chrono::duration_cast(time_end - time_start).count(); - auto ms = ns / 1000000.; - cout << "One filter time: " << ms << " ms" << endl; - - size_t pixels_in_range_2 = Kernels::filter_rgb32_euclidean( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out_2.data(), image_out_2.bytes_per_row(), - (uint32_t)COLOR_WHITE, !replace_color_within_range, - uint32_t(middle_color), max_dist - ); - - TEST_RESULT_EQUAL_STR(pixels_in_range, pixels_in_range_2); - - size_t actual_num_pixels_in_range = 0; - size_t error_count = 0; - for (size_t y = 0; y < height; y++){ - for (size_t x = 0; x < width; x++){ - const Color color(image.pixel(x, y)); - const Color new_color(image_out.pixel(x, y)); - const Color new_color_2(image_out_2.pixel(x, y)); - int red_dif = (color.red() - middle_color.red()); - int green_dif = (color.green() - middle_color.green()); - int blue_dif = (color.blue() - middle_color.blue()); - size_t color_dist2 = red_dif * red_dif + green_dif * green_dif + blue_dif * blue_dif; - bool in_range = color_dist2 <= max_dist2; - actual_num_pixels_in_range += in_range; - if (error_count < 10){ - // Print first 10 errors: - if (in_range && new_color != COLOR_WHITE){ - cout << "Error: wrong filter result: old color " << color.to_string() - << ", (x,y) = (" << x << ", " << y << ")" - << ", dist2 " << color_dist2 << ", max dist2 " << max_dist2 - << ", should be in range but not found by the function" << endl; - ++error_count; - }else if (in_range == false && new_color != color){ - cout << "Error: wrong filter result: old color " << color.to_string() - << ", (x,y) = (" << x << ", " << y << ")" - << ", dist2 " << color_dist2 << ", max dist2 " << max_dist2 - << ", should not be changed by the function" << endl; - ++error_count; - } - - if (in_range && new_color_2 != color){ - cout << "Error: wrong inverse filter result: old color " << color.to_string() - << ", (x,y) = (" << x << ", " << y << ")" - << ", dist2 " << color_dist2 << ", max dist2 " << max_dist2 - << ", should not be changed by the function" << endl; - ++error_count; - }else if (in_range == false && new_color_2 != COLOR_WHITE){ - cout << "Error: wrong inverse filter result: old color " << color.to_string() - << ", (x,y) = (" << x << ", " << y << ")" - << ", dist2 " << color_dist2 << ", max dist2 " << max_dist2 - << ", should not be in range but not found by the function" << endl; - ++error_count; - } - } - } - } - cout << "Found " << actual_num_pixels_in_range << " pixels in range" << endl; - if (pixels_in_range != actual_num_pixels_in_range){ - cout << "Error: wrong pixels in range: " << pixels_in_range << " actual: " << actual_num_pixels_in_range << endl; +// Whether `pixel` is in `range`. For example, 0xff7f3080 is in the range +// [0xff000000, 0xff8080ff], but 0xff813080 is not, since its R (0x81) is above 0x80. +bool in_range(uint32_t pixel, const ColorRange& range){ + for (int shift = 0; shift < 32; shift += 8){ + const uint32_t value = (pixel >> shift) & 0xff; + if (value < ((range.mins >> shift) & 0xff) || value > ((range.maxs >> shift) & 0xff)){ return false; } + } + return true; +} - if (error_count){ - return false; +size_t count_in_range(const GuardPageImageRGB32& image, const ColorRange& range){ + size_t count = 0; + for (size_t y = 0; y < image.height(); y++){ + for (size_t x = 0; x < image.width(); x++){ + count += in_range(image.pixel(x, y), range); } + } + return count; +} - // We try to wait for three seconds: - const size_t num_iters = size_t(3000 / ms); - time_start = current_time(); - for (size_t i = 0; i < num_iters; i++){ - pixels_in_range = Kernels::filter_rgb32_euclidean( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out.data(), image_out.bytes_per_row(), - (uint32_t)COLOR_WHITE, replace_color_within_range, - uint32_t(middle_color), max_dist - ); +// Compare each pixel of `output` with `expected_pixel()`. Return a +// description of the first wrong pixel, or an empty string if all pixels are right. +template +std::string find_wrong_pixel( + const GuardPageImageRGB32& input, const GuardPageImageRGB32& output, + ExpectedPixel expected_pixel +){ + for (size_t y = 0; y < input.height(); y++){ + for (size_t x = 0; x < input.width(); x++){ + const uint32_t pixel = input.pixel(x, y); + const uint32_t expected = expected_pixel(pixel); + if (output.pixel(x, y) != expected){ + return std::format( + "pixel ({}, {}) is {:08x}: got {:08x} but should be {:08x}", + x, y, pixel, output.pixel(x, y), expected + ); + } } - time_end = current_time(); - ms = (double)std::chrono::duration_cast(time_end - time_start).count(); - cout << "Running " << num_iters << " iters, avg filter time: " << ms / num_iters << " ms" << endl; + } + return ""; +} - return true; - - }; +std::string describe(const ColorRange& range){ + return std::format("range [{:08x}, {:08x}]", range.mins, range.maxs); +} -private: - std::string m_image; -}; -class Test_ImageToBlackWhiteRGB32Range : public UnitTest{ +// Check one implementation of `filter_rgb32_range()` and `to_blackwhite_rgb32_range()` +// on a real image, for each of `RANGES`, against `in_range()`: +// - `filter_rgb32_range()` copies the image, but replaces the pixels in range (or the +// pixels out of range) with `REPLACEMENT_COLOR`. +// - `to_blackwhite_rgb32_range()` makes the pixels in range black and the others white +// (or the opposite). +// Both must return the number of pixels in range. The input and output are guard-page +// images, so the test also crashes if the implementation reads or writes past the last +// pixel. +class Test_ImageFilterRGB32Range : public UnitTest{ public: - Test_ImageToBlackWhiteRGB32Range( + Test_ImageFilterRGB32Range( + const RangeFilterImplementation& implementation, const std::string& image ) - : UnitTest("Kernels::ImageToBlackWhiteRGB32Range - " + image) + : UnitTest("Kernels::ImageFilterRGB32Range - " + implementation.name + " - " + image) + , m_implementation(implementation) , m_image(UNIT_TEST_RESOURCE_PATH() + image) {} virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ - // Copy the test image into guard-page memory with packed rows, so a - // kernel that reads or writes past the last pixel faults. - ImageRGB32 file_image(m_image); - GuardPageImageRGB32 image(file_image); - - const size_t width = image.width(); - const size_t height = image.height(); - cout << "Testing to_black_white_rgb32_range(), image size " << width << " x " << height << endl; - - Color min_color(0, 0, 0); - // Color min_color(0); - - Color max_color(63, 63, 63); - // Color max_color(255, 255, 255); - // Color max_color(238, 24, 42); - cout << "min color: " << min_color.to_string() << " max color: " << max_color.to_string() << endl; - - const uint32_t mins = uint32_t(min_color); - const uint32_t maxs = uint32_t(max_color); - - GuardPageImageRGB32 image_out(image.width(), image.height()); - GuardPageImageRGB32 image_out_2(image.width(), image.height()); - size_t pixels_in_range = 0; - - const bool in_range_black = true; - auto time_start = current_time(); - // auto new_image = filter_rgb32_range(image, mins, maxs, COLOR_WHITE, replace_color_within_range); - pixels_in_range = Kernels::to_blackwhite_rgb32_range( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out.data(), image_out.bytes_per_row(), - in_range_black, - mins, maxs - ); - auto time_end = current_time(); - auto ns = std::chrono::duration_cast(time_end - time_start).count(); - auto ms = ns / 1000000.; - cout << "One filter time: " << ms << " ms" << endl; - - size_t pixels_in_range_2 = Kernels::to_blackwhite_rgb32_range( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out_2.data(), image_out_2.bytes_per_row(), - !in_range_black, - mins, maxs - ); - - TEST_RESULT_EQUAL_STR(pixels_in_range, pixels_in_range_2); - - size_t actual_num_pixels_in_range = 0; - size_t error_count = 0; - for (size_t y = 0; y < height; y++){ - for (size_t x = 0; x < width; x++){ - const Color color(image.pixel(x, y)); - const Color new_color(image_out.pixel(x, y)); - const Color new_color_2(image_out_2.pixel(x, y)); - bool in_range = (min_color.alpha() <= color.alpha() && color.alpha() <= max_color.alpha()); - in_range = in_range && (min_color.red() <= color.red() && color.red() <= max_color.red()); - in_range = in_range && (min_color.green() <= color.green() && color.green() <= max_color.green()); - in_range = in_range && (min_color.blue() <= color.blue() && color.blue() <= max_color.blue()); - actual_num_pixels_in_range += in_range; - if (error_count < 10){ - // Print first 10 errors: - if (in_range && new_color != COLOR_BLACK){ - cout << "Error: wrong filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should be black due to in range but not so" << endl; - ++error_count; - }else if (in_range == false && new_color != COLOR_WHITE){ - cout << "Error: wrong filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should be white due to out of range but not so" << endl; - ++error_count; - } - - if (in_range && new_color_2 != COLOR_WHITE){ - cout << "Error: wrong inverse filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should be white due to in range but not so" << endl; - ++error_count; - }else if (in_range == false && new_color_2 != COLOR_BLACK){ - cout << "Error: wrong inverse filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should be black due to out of range but not so" << endl; - ++error_count; - } + const ImageRGB32 file_image(m_image); + const GuardPageImageRGB32 image(file_image); + GuardPageImageRGB32 output(image.width(), image.height()); + + for (const ColorRange& range : RANGES){ + scope.throw_if_cancelled(); + const size_t expected_count = count_in_range(image, range); + + for (bool replace_color_within_range : {true, false}){ + const size_t count = m_implementation.filter( + image.data(), image.bytes_per_row(), image.width(), image.height(), + output.data(), output.bytes_per_row(), + REPLACEMENT_COLOR, replace_color_within_range, + range.mins, range.maxs + ); + const std::string error = find_wrong_pixel(image, output, [&](uint32_t pixel){ + return in_range(pixel, range) == replace_color_within_range ? REPLACEMENT_COLOR : pixel; + }); + const std::string name = std::format( + "filter_rgb32_range(), {}, replace_color_within_range = {}", + describe(range), replace_color_within_range + ); + if (count != expected_count){ + return std::format("Error: {}: counted {} pixels in range but should be {}.", name, count, expected_count); + } + if (!error.empty()){ + return std::format("Error: {}: {}.", name, error); } } - } - cout << "Found " << actual_num_pixels_in_range << " pixels in range" << endl; - if (pixels_in_range != actual_num_pixels_in_range){ - cout << "Error: wrong pixels in range: " << pixels_in_range << " actual: " << actual_num_pixels_in_range << endl; - return false; - } - - if (error_count){ - return false; - } - // We try to wait for three seconds: - const size_t num_iters = size_t(3000 / ms); - time_start = current_time(); - for (size_t i = 0; i < num_iters; i++){ - Kernels::to_blackwhite_rgb32_range( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out.data(), image_out.bytes_per_row(), - in_range_black, - mins, maxs - ); + for (bool in_range_black : {true, false}){ + const size_t count = m_implementation.to_blackwhite( + image.data(), image.bytes_per_row(), image.width(), image.height(), + output.data(), output.bytes_per_row(), + in_range_black, + range.mins, range.maxs + ); + const std::string error = find_wrong_pixel(image, output, [&](uint32_t pixel){ + return in_range(pixel, range) == in_range_black ? BLACK : WHITE; + }); + const std::string name = std::format( + "to_blackwhite_rgb32_range(), {}, in_range_black = {}", + describe(range), in_range_black + ); + if (count != expected_count){ + return std::format("Error: {}: counted {} pixels in range but should be {}.", name, count, expected_count); + } + if (!error.empty()){ + return std::format("Error: {}: {}.", name, error); + } + } } - time_end = current_time(); - ms = (double)std::chrono::duration_cast(time_end - time_start).count(); - cout << "Running " << num_iters << " iters, avg filter time: " << ms / num_iters << " ms" << endl; - return true; - - }; + } private: + RangeFilterImplementation m_implementation; std::string m_image; }; +// Check one `filter_by_mask()` implementation on a real image, for each of `RANGES`. +// The mask is a binary matrix of the implementation's type with a 1 bit for each pixel +// in range. It is made one bit at a time with `PackedBinaryMatrix_IB::set()`, so it does +// not depend on the SIMD code of `compress_rgb32_to_binary_range()`. Then +// `filter_by_mask()` must replace the pixels with 0 bits (or the pixels with 1 bits) with +// `REPLACEMENT_COLOR`, and keep the other pixels. The image is a guard-page image, so the +// test also crashes if the implementation reads or writes past the last pixel. class Test_ImageFilterByMask : public UnitTest{ public: Test_ImageFilterByMask( + const FilterByMaskImplementation& implementation, const std::string& image ) - : UnitTest("Kernels::ImageFilterByMask - " + image) + : UnitTest("Kernels::ImageFilterByMask - " + implementation.name + " - " + image) + , m_implementation(implementation) , m_image(UNIT_TEST_RESOURCE_PATH() + image) {} virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ - // Copy the test image into guard-page memory with packed rows, so a - // kernel that reads or writes past the last pixel faults. - ImageRGB32 file_image(m_image); - GuardPageImageRGB32 image(file_image); - - const size_t width = image.width(), height = image.height(); - cout << "Image width " << width << " height " << height << endl; - - const Color min_color(0, 0, 0), max_color(63, 63, 63); - const uint32_t mins = uint32_t(min_color), maxs = uint32_t(max_color); - - auto binary_matrix = make_PackedBinaryMatrix(get_BinaryMatrixType(), width, height); - compress_rgb32_to_binary_range( - image.data(), image.bytes_per_row(), *binary_matrix, mins, maxs - ); - GuardPageImageRGB32 new_image(file_image); - GuardPageImageRGB32 new_image_2(file_image); - - Color replacement_color = COLOR_WHITE; - bool replace_zero_bits = true; - - auto time_start = current_time(); - filter_by_mask(*binary_matrix, new_image.data(), new_image.bytes_per_row(), uint32_t(replacement_color), replace_zero_bits); - auto time_end = current_time(); - auto ns = std::chrono::duration_cast(time_end - time_start).count(); - auto ms = ns / 1000000.; - cout << "One Filter by mask call. time: " << ms << " ms" << endl; - - filter_by_mask(*binary_matrix, new_image_2.data(), new_image_2.bytes_per_row(), uint32_t(replacement_color), !replace_zero_bits); - - size_t error_count = 0; - - for (size_t y = 0; y < height; y++){ - for (size_t x = 0; x < width; x++){ - const Color color(image.pixel(x, y)); - const Color new_color(new_image.pixel(x, y)); - const Color new_color_2(new_image_2.pixel(x, y)); - bool in_range = (min_color.alpha() <= color.alpha() && color.alpha() <= max_color.alpha()); - in_range = in_range && (min_color.red() <= color.red() && color.red() <= max_color.red()); - in_range = in_range && (min_color.green() <= color.green() && color.green() <= max_color.green()); - in_range = in_range && (min_color.blue() <= color.blue() && color.blue() <= max_color.blue()); - - if (error_count <= 10){ - if (in_range && new_color != color){ - cout << "Error: wrong filter(replace_zero_bits) result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should not be changed due to being one bit but not so" << endl; - ++error_count; - }else if (!in_range && new_color != replacement_color){ - cout << "Error: wrong filter(replace_zero_bits) result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should be changed due to being zero bit but not so" << endl; - ++error_count; - } - - if (in_range && new_color_2 != replacement_color){ - cout << "Error: wrong filter(replace_one_bits) result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should be changed due to being one bit but not so" << endl; - ++error_count; - }else if (!in_range && new_color_2 != color){ - cout << "Error: wrong filter(replace_one_bits) result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should not be changed due to being zero bit but not so" << endl; - ++error_count; - } + const ImageRGB32 file_image(m_image); + const GuardPageImageRGB32 image(file_image); + + for (const ColorRange& range : RANGES){ + scope.throw_if_cancelled(); + std::unique_ptr mask = make_PackedBinaryMatrix( + m_implementation.matrix_type, image.width(), image.height() + ); + for (size_t y = 0; y < image.height(); y++){ + for (size_t x = 0; x < image.width(); x++){ + mask->set(x, y, in_range(image.pixel(x, y), range)); } } - } - - if (error_count){ - return false; - } - - // We try to wait for three seconds: - const size_t num_iters = size_t(3000 / ms); - time_start = current_time(); - for (size_t i = 0; i < num_iters; i++){ - filter_by_mask(*binary_matrix, new_image.data(), new_image.bytes_per_row(), uint32_t(replacement_color), replace_zero_bits); - } - time_end = current_time(); - ms = (double)std::chrono::duration_cast(time_end - time_start).count(); - cout << "Running " << num_iters << " iters, avg filter time: " << ms / num_iters << " ms" << endl; - - return true; - - }; - -private: - std::string m_image; -}; - -class Test_CompressRGB32ToBinaryEuclidean : public UnitTest{ -public: - Test_CompressRGB32ToBinaryEuclidean( - const std::string& image - ) - : UnitTest("Kernels::CompressRGB32ToBinaryEuclidean - " + image) - , m_image(UNIT_TEST_RESOURCE_PATH() + image) - {} - - virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ - // Copy the test image into guard-page memory with packed rows, so a - // kernel that reads or writes past the last pixel faults. - ImageRGB32 file_image(m_image); - GuardPageImageRGB32 image(file_image); - - const size_t width = image.width(); - const size_t height = image.height(); - cout << "Testing test_kernels_CompressRGB32ToBinaryEuclidean(), image size " << width << " x " << height << endl; - - Color middle_color = Color(image.pixel(width / 2, height / 2)); - cout << "Expected color: " << middle_color.to_string() << endl; - - double max_dist = 50.0; - size_t max_dist2 = size_t(max_dist * max_dist); - - PackedBinaryMatrix matrix(image.width(), image.height()); - - auto time_start = current_time(); - Kernels::compress_rgb32_to_binary_euclidean( - image.data(), image.bytes_per_row(), matrix, - uint32_t(middle_color), max_dist - ); - auto time_end = current_time(); - auto ns = std::chrono::duration_cast(time_end - time_start).count(); - auto ms = ns / 1000000.; - cout << "One filter time: " << ms << " ms" << endl; - - size_t error_count = 0; - for (size_t y = 0; y < height; y++){ - for (size_t x = 0; x < width; x++){ - const Color color(image.pixel(x, y)); - int red_dif = (color.red() - middle_color.red()); - int green_dif = (color.green() - middle_color.green()); - int blue_dif = (color.blue() - middle_color.blue()); - size_t color_dist2 = red_dif * red_dif + green_dif * green_dif + blue_dif * blue_dif; - bool in_range = color_dist2 <= max_dist2; - if (error_count < 10){ - // Print first 10 errors: - if (in_range && matrix.get(x, y) == false){ - cout << "Error: wrong filter result: old color " << color.to_string() - << ", (x,y) = (" << x << ", " << y << ")" - << ", dist2 " << color_dist2 << ", max dist2 " << max_dist2 - << ", should be in range but not set on matrix" << endl; - ++error_count; - }else if (in_range == false && matrix.get(x, y) == true){ - cout << "Error: wrong filter result: old color " << color.to_string() - << ", (x,y) = (" << x << ", " << y << ")" - << ", dist2 " << color_dist2 << ", max dist2 " << max_dist2 - << ", should not be in range but set on matrix" << endl; - ++error_count; - } + for (bool replace_if_zero : {true, false}){ + GuardPageImageRGB32 filtered(file_image); + m_implementation.filter_by_mask( + *mask, filtered.data(), filtered.bytes_per_row(), + REPLACEMENT_COLOR, replace_if_zero + ); + // The mask bit is 1 if the pixel is in range. The pixel is replaced if + // that bit is 0 and `replace_if_zero` is true, or if it is 1 and + // `replace_if_zero` is false. + const std::string error = find_wrong_pixel(image, filtered, [&](uint32_t pixel){ + return in_range(pixel, range) != replace_if_zero ? REPLACEMENT_COLOR : pixel; + }); + if (!error.empty()){ + return std::format( + "Error: mask of {}, replace_if_zero = {}: {}.", + describe(range), replace_if_zero, error + ); } } } - if (error_count){ - return false; - } - - // We try to wait for three seconds: - const size_t num_iters = size_t(3000 / ms); - time_start = current_time(); - for (size_t i = 0; i < num_iters; i++){ - Kernels::compress_rgb32_to_binary_euclidean( - image.data(), image.bytes_per_row(), matrix, - uint32_t(middle_color), max_dist - ); - } - time_end = current_time(); - ms = (double)std::chrono::duration_cast(time_end - time_start).count(); - cout << "Running " << num_iters << " iters, avg filter time: " << ms / num_iters << " ms" << endl; - return true; - }; + } private: + FilterByMaskImplementation m_implementation; std::string m_image; }; +} - +// Add a test for each image and each range filter and filter_by_mask() implementation +// that this build has and this CPU supports, including Default. void add_tests_ImageFilters(UnitTestDatabase& database){ + std::vector range_filters{ + {"Default", filter_rgb32_range_Default, to_blackwhite_rgb32_range_Default}, + }; + std::vector mask_filters{ + {"64x4_Default", BinaryMatrixType::i64x4_Default, filter_by_mask_64x4_Default}, + }; +#ifdef PA_AutoDispatch_x64_08_Nehalem + if (CPU_CAPABILITY_CURRENT.OK_08_Nehalem){ + range_filters.push_back({"x64_SSE42", filter_rgb32_range_x64_SSE42, to_blackwhite_rgb32_range_x64_SSE42}); + mask_filters.push_back({"64x8_x64_SSE42", BinaryMatrixType::i64x8_x64_SSE42, filter_by_mask_64x8_x64_SSE42}); + } +#endif +#ifdef PA_AutoDispatch_x64_13_Haswell + if (CPU_CAPABILITY_CURRENT.OK_13_Haswell){ + range_filters.push_back({"x64_AVX2", filter_rgb32_range_x64_AVX2, to_blackwhite_rgb32_range_x64_AVX2}); + mask_filters.push_back({"64x16_x64_AVX2", BinaryMatrixType::i64x16_x64_AVX2, filter_by_mask_64x16_x64_AVX2}); + } +#endif +#ifdef PA_AutoDispatch_x64_17_Skylake + if (CPU_CAPABILITY_CURRENT.OK_17_Skylake){ + range_filters.push_back({"x64_AVX512", filter_rgb32_range_x64_AVX512, to_blackwhite_rgb32_range_x64_AVX512}); + mask_filters.push_back({"64x32_x64_AVX512", BinaryMatrixType::i64x32_x64_AVX512, filter_by_mask_64x32_x64_AVX512}); + mask_filters.push_back({"64x64_x64_AVX512", BinaryMatrixType::i64x64_x64_AVX512, filter_by_mask_64x64_x64_AVX512}); + } +#endif +#ifdef PA_AutoDispatch_arm64_20_M1 + if (CPU_CAPABILITY_CURRENT.OK_M1){ + range_filters.push_back({"arm64_NEON", filter_rgb32_range_arm64_NEON, to_blackwhite_rgb32_range_arm64_NEON}); + mask_filters.push_back({"64x8_arm64_NEON", BinaryMatrixType::arm64x8_x64_NEON, filter_by_mask_64x8_arm64_NEON}); + } +#endif + // Images in the CommandLineTests repo. Their widths cover every remainder mod 4, and + // no width or height is a multiple of 8, so the leftover pixels after the SIMD loops + // and the binary matrix tiles at the right and bottom edges are tested too. + const std::vector images{ + "NintendoSwitch/Switch2/ghostassassin/system_settings_display_dark.png", // 1916 x 1078 + "OCR/Lv.1-0.png", // 237 x 77 + "PokemonSwSh/SelectionArrowFinder/mac_neon_error_1.png", // 106 x 410 + "OCR/german-nature-sanft.png", // 511 x 91 + }; - + for (const std::string& image : images){ + for (const RangeFilterImplementation& implementation : range_filters){ + database.add(implementation, image); + } + for (const FilterByMaskImplementation& implementation : mask_filters){ + database.add(implementation, image); + } + } } From 2fc9680c7d45a25aa9dc369d379450154aaf4bcc Mon Sep 17 00:00:00 2001 From: Gin <> Date: Wed, 21 Oct 2026 23:01:22 -0700 Subject: [PATCH 2/3] Fix the replace flag of the multi-filter filter_rgb32_range() The overload that runs several range filters in one call passed the filter's pixels_in_range, which is not set yet, as replace_color_within_range. It should pass the filter's own flag, invert. This broke in 5bb2f457b. The PokemonLA MountDetector uses this overload. Also set pixels_in_range to 0 in both multi-filter structs. Co-Authored-By: Claude Opus 5.5 --- .../RGB32_Range/Kernels_ImageFilter_RGB32_Range.cpp | 2 +- .../ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.cpp b/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.cpp index 0c5943a5ec..12e94686ac 100644 --- a/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.cpp +++ b/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.cpp @@ -171,7 +171,7 @@ void filter_rgb32_range( image, bytes_per_row, width, height, filter[c].data, filter[c].bytes_per_row, filter[c].replacement, - filter[c].pixels_in_range, + filter[c].invert, filter[c].mins, filter[c].maxs ); diff --git a/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.h b/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.h index d312e22151..23c9ddfc10 100644 --- a/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.h +++ b/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.h @@ -52,6 +52,7 @@ struct FilterRgb32RangeFilter{ , invert(p_invert) , mins(p_mins) , maxs(p_maxs) + , pixels_in_range(0) {} }; void filter_rgb32_range( @@ -94,6 +95,7 @@ struct ToBlackWhiteRgb32RangeFilter{ , mins(p_mins) , maxs(p_maxs) , in_range_black(p_in_range_black) + , pixels_in_range(0) {} }; void to_blackwhite_rgb32_range( From 786dfd91477def6d5eae636c503bba0d39e44ce3 Mon Sep 17 00:00:00 2001 From: Gin <> Date: Wed, 21 Oct 2026 23:02:45 -0700 Subject: [PATCH 3/3] Test the multi-filter overloads of the range filters Add a test that runs filter_rgb32_range() and to_blackwhite_rgb32_range() with 8 filters in one call, and checks each output against the scalar reference. It fails without the fix in the previous commit. Co-Authored-By: Claude Opus 5.5 --- .../Kernels_ImageFilter_Tests.cpp | 149 +++++++++++++++--- 1 file changed, 126 insertions(+), 23 deletions(-) diff --git a/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp b/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp index 511c7b172d..f35c15f57c 100644 --- a/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp +++ b/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp @@ -12,6 +12,7 @@ #include "CommonFramework/GlobalAutoPaths.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" #include "Kernels/BinaryMatrix/Kernels_BinaryMatrix.h" +#include "RGB32_Range/Kernels_ImageFilter_RGB32_Range.h" #include "Tests/GuardPageImage.h" #include "Kernels_ImageFilter_Tests.h" @@ -201,6 +202,36 @@ std::string describe(const ColorRange& range){ return std::format("range [{:08x}, {:08x}]", range.mins, range.maxs); } +// Check one result of `filter_rgb32_range()` on `image`: the number of pixels in range +// that it returned, and its output image. Return a description of the first problem, or +// an empty string if there is none. +std::string check_filter_result( + const GuardPageImageRGB32& image, const GuardPageImageRGB32& output, size_t count, + const ColorRange& range, bool replace_color_within_range +){ + const size_t expected_count = count_in_range(image, range); + if (count != expected_count){ + return std::format("counted {} pixels in range but should be {}", count, expected_count); + } + return find_wrong_pixel(image, output, [&](uint32_t pixel){ + return in_range(pixel, range) == replace_color_within_range ? REPLACEMENT_COLOR : pixel; + }); +} + +// Same as `check_filter_result()`, for one result of `to_blackwhite_rgb32_range()`. +std::string check_blackwhite_result( + const GuardPageImageRGB32& image, const GuardPageImageRGB32& output, size_t count, + const ColorRange& range, bool in_range_black +){ + const size_t expected_count = count_in_range(image, range); + if (count != expected_count){ + return std::format("counted {} pixels in range but should be {}", count, expected_count); + } + return find_wrong_pixel(image, output, [&](uint32_t pixel){ + return in_range(pixel, range) == in_range_black ? BLACK : WHITE; + }); +} + // Check one implementation of `filter_rgb32_range()` and `to_blackwhite_rgb32_range()` @@ -230,7 +261,6 @@ class Test_ImageFilterRGB32Range : public UnitTest{ for (const ColorRange& range : RANGES){ scope.throw_if_cancelled(); - const size_t expected_count = count_in_range(image, range); for (bool replace_color_within_range : {true, false}){ const size_t count = m_implementation.filter( @@ -239,18 +269,12 @@ class Test_ImageFilterRGB32Range : public UnitTest{ REPLACEMENT_COLOR, replace_color_within_range, range.mins, range.maxs ); - const std::string error = find_wrong_pixel(image, output, [&](uint32_t pixel){ - return in_range(pixel, range) == replace_color_within_range ? REPLACEMENT_COLOR : pixel; - }); - const std::string name = std::format( - "filter_rgb32_range(), {}, replace_color_within_range = {}", - describe(range), replace_color_within_range - ); - if (count != expected_count){ - return std::format("Error: {}: counted {} pixels in range but should be {}.", name, count, expected_count); - } + const std::string error = check_filter_result(image, output, count, range, replace_color_within_range); if (!error.empty()){ - return std::format("Error: {}: {}.", name, error); + return std::format( + "Error: filter_rgb32_range(), {}, replace_color_within_range = {}: {}.", + describe(range), replace_color_within_range, error + ); } } @@ -261,18 +285,12 @@ class Test_ImageFilterRGB32Range : public UnitTest{ in_range_black, range.mins, range.maxs ); - const std::string error = find_wrong_pixel(image, output, [&](uint32_t pixel){ - return in_range(pixel, range) == in_range_black ? BLACK : WHITE; - }); - const std::string name = std::format( - "to_blackwhite_rgb32_range(), {}, in_range_black = {}", - describe(range), in_range_black - ); - if (count != expected_count){ - return std::format("Error: {}: counted {} pixels in range but should be {}.", name, count, expected_count); - } + const std::string error = check_blackwhite_result(image, output, count, range, in_range_black); if (!error.empty()){ - return std::format("Error: {}: {}.", name, error); + return std::format( + "Error: to_blackwhite_rgb32_range(), {}, in_range_black = {}: {}.", + describe(range), in_range_black, error + ); } } } @@ -286,6 +304,90 @@ class Test_ImageFilterRGB32Range : public UnitTest{ +// Check the overloads of `filter_rgb32_range()` and `to_blackwhite_rgb32_range()` that +// run several filters in one call, like `MountDetector` in PokemonLA does. They call the +// single-filter versions through the CPU dispatch, so this test runs once, not once per +// implementation. There is one filter for each of `RANGES` and each flag value, so the +// test also checks that each filter uses its own range and flag. +class Test_ImageFilterRGB32RangeMultiple : public UnitTest{ +public: + Test_ImageFilterRGB32RangeMultiple(const std::string& image) + : UnitTest("Kernels::ImageFilterRGB32Range - multiple filters - " + image) + , m_image(UNIT_TEST_RESOURCE_PATH() + image) + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ + const ImageRGB32 file_image(m_image); + const GuardPageImageRGB32 image(file_image); + + struct FilterSetting{ + ColorRange range; + bool flag; + }; + std::vector settings; + std::vector> outputs; + for (const ColorRange& range : RANGES){ + for (bool flag : {true, false}){ + settings.push_back({range, flag}); + outputs.emplace_back(std::make_unique(image.width(), image.height())); + } + } + + std::vector filters; + for (size_t c = 0; c < settings.size(); c++){ + filters.emplace_back( + outputs[c]->data(), outputs[c]->bytes_per_row(), + REPLACEMENT_COLOR, settings[c].flag, + settings[c].range.mins, settings[c].range.maxs + ); + } + filter_rgb32_range( + image.data(), image.bytes_per_row(), image.width(), image.height(), + filters.data(), filters.size() + ); + for (size_t c = 0; c < settings.size(); c++){ + const std::string error = check_filter_result( + image, *outputs[c], filters[c].pixels_in_range, settings[c].range, settings[c].flag + ); + if (!error.empty()){ + return std::format( + "Error: filter_rgb32_range() with {} filters, {}, replace_color_within_range = {}: {}.", + filters.size(), describe(settings[c].range), settings[c].flag, error + ); + } + } + + std::vector blackwhite_filters; + for (size_t c = 0; c < settings.size(); c++){ + blackwhite_filters.emplace_back( + outputs[c]->data(), outputs[c]->bytes_per_row(), + settings[c].range.mins, settings[c].range.maxs, settings[c].flag + ); + } + to_blackwhite_rgb32_range( + image.data(), image.bytes_per_row(), image.width(), image.height(), + blackwhite_filters.data(), blackwhite_filters.size() + ); + for (size_t c = 0; c < settings.size(); c++){ + const std::string error = check_blackwhite_result( + image, *outputs[c], blackwhite_filters[c].pixels_in_range, settings[c].range, settings[c].flag + ); + if (!error.empty()){ + return std::format( + "Error: to_blackwhite_rgb32_range() with {} filters, {}, in_range_black = {}: {}.", + blackwhite_filters.size(), describe(settings[c].range), settings[c].flag, error + ); + } + } + return true; + } + +private: + std::string m_image; +}; + + + // Check one `filter_by_mask()` implementation on a real image, for each of `RANGES`. // The mask is a binary matrix of the implementation's type with a 1 bit for each pixel // in range. It is made one bit at a time with `PackedBinaryMatrix_IB::set()`, so it does @@ -396,6 +498,7 @@ void add_tests_ImageFilters(UnitTestDatabase& database){ "OCR/german-nature-sanft.png", // 511 x 91 }; + database.add(images[2]); for (const std::string& image : images){ for (const RangeFilterImplementation& implementation : range_filters){ database.add(implementation, image);