diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_DialogDetector.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_DialogDetector.cpp index c8636d99cb..d3cbe175b1 100644 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_DialogDetector.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_DialogDetector.cpp @@ -4,8 +4,13 @@ * */ +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "CommonFramework/VideoPipeline/VideoOverlay.h" #include "CommonTools/Images/SolidColorTest.h" +#include "Tests/ImageUnitTest.h" #include "PokemonBDSP_DialogDetector.h" //#include @@ -23,6 +28,7 @@ ShortDialogDetector::ShortDialogDetector(Color color) , m_left_white(0.21, 0.835, 0.008, 0.12) , m_left(0.18, 0.835, 0.02, 0.12) , m_right_white(0.785, 0.835, 0.008, 0.12) + , m_right_white_low(0.785, 0.915, 0.008, 0.04) , m_right(0.822, 0.835, 0.02, 0.12) {} void ShortDialogDetector::make_overlays(VideoOverlaySet& items) const{ @@ -30,6 +36,7 @@ void ShortDialogDetector::make_overlays(VideoOverlaySet& items) const{ items.add(m_color, m_left_white); items.add(m_color, m_left); items.add(m_color, m_right_white); + items.add(m_color, m_right_white_low); items.add(m_color, m_right); } bool ShortDialogDetector::detect(const ImageViewRGB32& screen){ @@ -40,7 +47,9 @@ bool ShortDialogDetector::detect(const ImageViewRGB32& screen){ } ImageStats right_white = image_stats(extract_box_reference(screen, m_right_white)); // cout << right_white.average << right_white.stddev << endl; - if (!is_white(right_white)){ + // A long first line runs into m_right_white (Rowan's "Now, go on, and + // leap into the world of Pokemon!"). Check below that line as well. + if (!is_white(right_white) && !is_white(image_stats(extract_box_reference(screen, m_right_white_low)))){ return false; } // ImageStats bottom = image_stats(extract_box_reference(screen, m_bottom)); @@ -125,6 +134,63 @@ bool ShortDialogPromptDetector::process_frame(const ImageViewRGB32& screen, Wall + + +namespace{ + +// The captured screen supplied to the short-dialog detector. +struct DialogInput{ + std::string_view image; +}; +// Whether a short dialog should be visible in that screen. +struct DialogExpectation{ + bool detected = false; +}; +// One image and its independently specified expected result. +struct DialogCase{ + DialogInput input; + DialogExpectation expectation; +}; + +// Runs the detector on the original image and the image-test color distortions. +class Test_ShortDialogDetector : public ImageUnitTest{ +public: + explicit Test_ShortDialogDetector(const DialogCase& test) + : ImageUnitTest(std::format("PokemonBDSP::ShortDialogDetector - {}", test.input.image), {std::string(test.input.image)}) + , m_expectation(test.expectation) + {} + + virtual UnitTestResult run_on_images( + Logger&, CancellableScope&, + const std::vector& images + ) const override{ + const bool detected = ShortDialogDetector().detect(images[0]); + if (detected != m_expectation.detected){ + return std::format("Expected short dialog detected={}, read {}.", m_expectation.detected, detected); + } + return true; + } + +private: + DialogExpectation m_expectation; +}; + +} // namespace + +void add_tests_DialogDetector(UnitTestDatabase& database){ + static constexpr std::array CASES{{ + {.input = {.image = "PokemonBDSP/DialogDetector/IntroRowanNameTab_True.png"}, .expectation = {.detected = true}}, + {.input = {.image = "PokemonBDSP/DialogDetector/IntroRowanNoArrow_True.png"}, .expectation = {.detected = true}}, + {.input = {.image = "PokemonBDSP/DialogDetector/IntroRowanShortLine_True.png"}, .expectation = {.detected = true}}, + {.input = {.image = "PokemonBDSP/DialogDetector/JubilifeUnknownNameTab_True.png"}, .expectation = {.detected = true}}, + {.input = {.image = "PokemonBDSP/DialogDetector/FloaromaOldManOneLine_True.png"}, .expectation = {.detected = true}}, + {.input = {.image = "PokemonBDSP/DialogDetector/FloaromaCloudShadows_False.png"}, .expectation = {.detected = false}}, + }}; + for (const DialogCase& test : CASES){ + database.add(test); + } } -} -} + +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h index fe7c90cbb0..00d4b4eb1f 100644 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h @@ -7,6 +7,7 @@ #ifndef PokemonAutomation_PokemonBDSP_BattleDialogDetector_H #define PokemonAutomation_PokemonBDSP_BattleDialogDetector_H +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" #include "CommonTools/VisualDetector.h" #include "PokemonBDSP_SelectionArrow.h" @@ -31,6 +32,7 @@ class ShortDialogDetector : public StaticScreenDetector{ ImageFloatBox m_left_white; ImageFloatBox m_left; ImageFloatBox m_right_white; + ImageFloatBox m_right_white_low; ImageFloatBox m_right; }; class ShortDialogWatcher : public DetectorToFinder{ @@ -79,7 +81,11 @@ class ShortDialogPromptDetector : public VisualInferenceCallback{ -} -} -} + +// The screenshot tests in CommandLineTests/PokemonBDSP/DialogDetector/. +void add_tests_DialogDetector(UnitTestDatabase& database); + +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.cpp index 12da43966e..6d23f8fb83 100644 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.cpp @@ -4,9 +4,14 @@ * */ +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "CommonFramework/ImageTools/ImageStats.h" #include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" #include "CommonTools/Images/SolidColorTest.h" +#include "Tests/ImageUnitTest.h" #include "PokemonBDSP_MenuDetector.h" namespace PokemonAutomation{ @@ -14,15 +19,23 @@ namespace NintendoSwitch{ namespace PokemonBDSP{ -const double STRIPE_TOP = 0.110; -const double STRIPE_TOP_BELOW_BANNER = 0.170; -const double STRIPE_BOTTOM = 0.598; +namespace{ -static ImageFloatBox menu_stripe(size_t index, bool skip_new_banner){ +constexpr double STRIPE_TOP = 0.110; +constexpr double STRIPE_TOP_BELOW_BANNER = 0.170; +constexpr double STRIPE_BOTTOM = 0.598; + +// Each column sits in the gap right of an icon, clear of the "NEW" tag on +// its top right corner. The tag on TOWN MAP extends into a column at 0.160. +constexpr double STRIPE_LEFT = 0.166; + +ImageFloatBox menu_stripe(size_t index, bool skip_new_banner){ double top = skip_new_banner ? STRIPE_TOP_BELOW_BANNER : STRIPE_TOP; - return ImageFloatBox(0.160 + 0.166 * (double)index, top, 0.015, STRIPE_BOTTOM - top); + return ImageFloatBox(STRIPE_LEFT + 0.166 * (double)index, top, 0.015, STRIPE_BOTTOM - top); } +} // namespace + MenuDetector::MenuDetector(Color color, bool skip_new_banner) : m_color(color) , m_line0(menu_stripe(0, skip_new_banner)) @@ -84,6 +97,108 @@ bool MenuWatcher::process_frame(const ImageViewRGB32& frame, WallClock timestamp +namespace{ + +// Selects full-height columns or columns starting below the NEW tags. +enum class Columns{ + FromTop, + BelowNewTags, +}; +// A screen and the existing detector mode used to inspect it. +struct MenuInput{ + std::string_view image; + Columns columns = Columns::FromTop; +}; +// Whether the screen should be recognized as the X menu. +struct MenuExpectation{ + bool detected = false; +}; +// One screen/mode pair and its independently specified expected result. +struct MenuCase{ + MenuInput input; + MenuExpectation expectation; +}; + +// Verifies both detector modes, including the image-test color distortions. +class Test_MenuDetector : public ImageUnitTest{ +public: + explicit Test_MenuDetector(const MenuCase& test) + : ImageUnitTest( + std::format("PokemonBDSP::MenuDetector{} - {}", + test.input.columns == Columns::BelowNewTags ? "(skip_new_banner)" : "", test.input.image), + {std::string(test.input.image)} + ) + , m_columns(test.input.columns) + , m_expectation(test.expectation) + {} + + virtual UnitTestResult run_on_images( + Logger&, CancellableScope&, + const std::vector& images + ) const override{ + MenuDetector detector(COLOR_RED, /*skip_new_banner=*/m_columns == Columns::BelowNewTags); + const bool detected = detector.detect(images[0]); + if (detected != m_expectation.detected){ + return std::format("Expected menu detected={}, read {}.", m_expectation.detected, detected); + } + return true; + } + +private: + Columns m_columns = Columns::FromTop; + MenuExpectation m_expectation; +}; + +} // namespace + +void add_tests_MenuDetector(UnitTestDatabase& database){ + static constexpr std::array CASES{{ + {.input = {.image = "PokemonBDSP/MenuDetector/NewGameBagOptions_True.png", .columns = Columns::FromTop}, + .expectation = {.detected = true}}, + {.input = {.image = "PokemonBDSP/MenuDetector/NewGameBagOptions_True.png", .columns = Columns::BelowNewTags}, + .expectation = {.detected = true}}, + {.input = {.image = "PokemonBDSP/MenuDetector/JubilifeTownMapNew_True.png", .columns = Columns::FromTop}, + .expectation = {.detected = true}}, + {.input = {.image = "PokemonBDSP/MenuDetector/JubilifeTownMapNew_True.png", .columns = Columns::BelowNewTags}, + .expectation = {.detected = true}}, + {.input = {.image = "PokemonBDSP/MenuDetector/FloaromaMenuThreeNew_True.png", .columns = Columns::FromTop}, + .expectation = {.detected = true}}, + {.input = {.image = "PokemonBDSP/MenuDetector/FloaromaMenuThreeNew_True.png", .columns = Columns::BelowNewTags}, + .expectation = {.detected = true}}, + {.input = {.image = "PokemonBDSP/MenuDetector/Home_False.png", .columns = Columns::FromTop}, + .expectation = {.detected = false}}, + {.input = {.image = "PokemonBDSP/MenuDetector/Home_False.png", .columns = Columns::BelowNewTags}, + .expectation = {.detected = false}}, + {.input = {.image = "PokemonBDSP/MenuDetector/OverworldSandgemMart_False.png", .columns = Columns::FromTop}, + .expectation = {.detected = false}}, + {.input = {.image = "PokemonBDSP/MenuDetector/OverworldSandgemMart_False.png", .columns = Columns::BelowNewTags}, + .expectation = {.detected = false}}, + {.input = {.image = "PokemonBDSP/MenuDetector/OverworldOreburghMine_False.png", .columns = Columns::FromTop}, + .expectation = {.detected = false}}, + {.input = {.image = "PokemonBDSP/MenuDetector/OverworldOreburghMine_False.png", .columns = Columns::BelowNewTags}, + .expectation = {.detected = false}}, + {.input = {.image = "PokemonBDSP/MenuDetector/IntroDialog_False.png", .columns = Columns::FromTop}, + .expectation = {.detected = false}}, + {.input = {.image = "PokemonBDSP/MenuDetector/IntroDialog_False.png", .columns = Columns::BelowNewTags}, + .expectation = {.detected = false}}, + {.input = {.image = "PokemonBDSP/MenuDetector/IntroBarry_False.png", .columns = Columns::FromTop}, + .expectation = {.detected = false}}, + {.input = {.image = "PokemonBDSP/MenuDetector/IntroBarry_False.png", .columns = Columns::BelowNewTags}, + .expectation = {.detected = false}}, + {.input = {.image = "PokemonBDSP/MenuDetector/FloaromaOldManDialog_False.png", .columns = Columns::FromTop}, + .expectation = {.detected = false}}, + {.input = {.image = "PokemonBDSP/MenuDetector/FloaromaOldManDialog_False.png", .columns = Columns::BelowNewTags}, + .expectation = {.detected = false}}, + {.input = {.image = "PokemonBDSP/MenuDetector/FloaromaCloudShadows_False.png", .columns = Columns::FromTop}, + .expectation = {.detected = false}}, + {.input = {.image = "PokemonBDSP/MenuDetector/FloaromaCloudShadows_False.png", .columns = Columns::BelowNewTags}, + .expectation = {.detected = false}}, + }}; + for (const MenuCase& test : CASES){ + database.add(test); + } } -} -} + +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h index f7e5a0517f..e084f66d3e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h @@ -7,6 +7,7 @@ #ifndef PokemonAutomation_PokemonBDSP_MenuDetector_H #define PokemonAutomation_PokemonBDSP_MenuDetector_H +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "Common/Cpp/Color.h" #include "CommonFramework/ImageTools/ImageBoxes.h" #include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" @@ -46,7 +47,10 @@ class MenuWatcher : public MenuDetector, public VisualInferenceCallback{ -} -} -} +// The screenshot tests in CommandLineTests/PokemonBDSP/MenuDetector/. +void add_tests_MenuDetector(UnitTestDatabase& database); + +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index 145096b969..7f5bca1843 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -5,6 +5,8 @@ */ #include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "Inference/PokemonBDSP_DialogDetector.h" +#include "Inference/PokemonBDSP_MenuDetector.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Maps_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_RoutePlanner_Tests.h" @@ -21,6 +23,8 @@ namespace PokemonBDSP{ void add_tests(UnitTestDatabase& database){ + add_tests_DialogDetector(database); + add_tests_MenuDetector(database); add_tests_AutoStory_Maps(database); add_tests_AutoStory_StoryWorld(database); add_tests_AutoStory_RoutePlanner(database);