From b3d3fb1eddc13c6d167afa8a22f1193ab6438b25 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Sun, 26 Apr 2026 08:38:58 -0500 Subject: [PATCH 1/7] Added proper detection for gmax and tera type --- .../Pokemon/Options/Pokemon_BoxSortingTable.h | 3 +- .../Pokemon/Pokemon_CollectedPokemonInfo.cpp | 28 +++--- .../Pokemon/Pokemon_CollectedPokemonInfo.h | 5 +- .../PokemonHome_GigantamaxDetector.cpp | 90 +++++++++++++++++++ .../PokemonHome_GigantamaxDetector.h | 57 ++++++++++++ .../Programs/PokemonHome_BoxSorter.cpp | 52 ++++++++--- .../Programs/PokemonLZA_BoxSorter.cpp | 6 +- SerialPrograms/cmake/SourceFiles.cmake | 2 + 8 files changed, 217 insertions(+), 26 deletions(-) create mode 100644 SerialPrograms/Source/PokemonHome/Inference/PokemonHome_GigantamaxDetector.cpp create mode 100644 SerialPrograms/Source/PokemonHome/Inference/PokemonHome_GigantamaxDetector.h diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_BoxSortingTable.h b/SerialPrograms/Source/Pokemon/Options/Pokemon_BoxSortingTable.h index a8a5320169..8e2de08c5b 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_BoxSortingTable.h +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_BoxSortingTable.h @@ -23,7 +23,8 @@ enum class SortingRuleType Alpha, Ball_Slug, Gender, - Type + Type, + Tera_Type, }; struct SortingRule diff --git a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp index b2dc725640..ae02e69b8e 100644 --- a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp +++ b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp @@ -27,8 +27,9 @@ bool operator==(const CollectedPokemonInfo& lhs, const CollectedPokemonInfo& rhs lhs.ball_slug == rhs.ball_slug && lhs.gender == rhs.gender && lhs.ot_id == rhs.ot_id && - lhs.primaryType == rhs.primaryType && - lhs.secondaryType == rhs.secondaryType; + lhs.primary_type == rhs.primary_type && + lhs.secondary_type == rhs.secondary_type && + lhs.tera_type == rhs.tera_type; } @@ -75,11 +76,16 @@ bool operator<(const std::optional& lhs, const std::option } break; case SortingRuleType::Type: - if (lhs->primaryType != rhs->primaryType){ - return (lhs->primaryType < rhs->primaryType) != preference.reverse; + if (lhs->primary_type != rhs->primary_type){ + return (lhs->primary_type < rhs->primary_type) != preference.reverse; } - if (lhs->secondaryType != rhs->secondaryType){ - return (lhs->secondaryType < rhs->secondaryType) != preference.reverse; + if (lhs->secondary_type != rhs->secondary_type){ + return (lhs->secondary_type < rhs->secondary_type) != preference.reverse; + } + break; + case SortingRuleType::Tera_Type: + if (lhs->tera_type != rhs->tera_type){ + return (lhs->tera_type < rhs->tera_type) != preference.reverse; } break; default: @@ -103,8 +109,9 @@ std::ostream& operator<<(std::ostream& os, const std::optionalball_slug << " "; os << "gender:" << gender_to_string(pokemon->gender) << " "; os << "ot_id:" << pokemon->ot_id << " "; - os << "primaryType:" << POKEMON_TYPE_SLUGS().get_string(pokemon->primaryType) << " "; - os << "secondaryType:" << POKEMON_TYPE_SLUGS().get_string(pokemon->secondaryType) << " "; + os << "primaryType:" << POKEMON_TYPE_SLUGS().get_string(pokemon->primary_type) << " "; + os << "secondaryType:" << POKEMON_TYPE_SLUGS().get_string(pokemon->secondary_type) << " "; + os << "teraType:" << POKEMON_TYPE_SLUGS().get_string(pokemon->tera_type) << " "; os << ")"; }else{ os << "(empty)"; @@ -155,8 +162,9 @@ void save_boxes_data_to_json(const std::vectorball_slug; pokemon["gender"] = gender_to_string(current_pokemon->gender); pokemon["ot_id"] = current_pokemon->ot_id; - pokemon["primaryType"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->primaryType); - pokemon["secondaryType"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->secondaryType); + pokemon["primary_type"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->primary_type); + pokemon["secondary_type"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->secondary_type); + pokemon["tera_type"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->tera_type); } pokemon_data.push_back(std::move(pokemon)); } diff --git a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h index 7bd6117f0c..c71008c439 100644 --- a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h +++ b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h @@ -32,8 +32,9 @@ struct CollectedPokemonInfo{ std::string ball_slug = ""; StatsHuntGenderFilter gender = StatsHuntGenderFilter::Genderless; uint32_t ot_id = 0; // original trainer ID - PokemonType primaryType = PokemonType::NONE; - PokemonType secondaryType = PokemonType::NONE; + PokemonType primary_type = PokemonType::NONE; + PokemonType secondary_type = PokemonType::NONE; + PokemonType tera_type = PokemonType::NONE; }; bool operator==(const CollectedPokemonInfo& lhs, const CollectedPokemonInfo& rhs); diff --git a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_GigantamaxDetector.cpp b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_GigantamaxDetector.cpp new file mode 100644 index 0000000000..4ac8da8414 --- /dev/null +++ b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_GigantamaxDetector.cpp @@ -0,0 +1,90 @@ +/* Gigantamax Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Kernels/Waterfill/Kernels_Waterfill_Types.h" +#include "CommonTools/ImageMatch/WaterfillTemplateMatcher.h" +#include "CommonTools/Images/WaterfillUtilities.h" +#include "PokemonHome_GigantamaxDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonHome{ + + +class GigantamaxMatcher : public ImageMatch::WaterfillTemplateMatcher{ +public: + GigantamaxMatcher() + : WaterfillTemplateMatcher( + "PokemonHome/Home_Gigantamax.png", + Color(196, 32, 88), Color(255, 220, 235), + 100 + ) + { + m_aspect_ratio_lower = 0.9; + m_aspect_ratio_upper = 1.1; + m_area_ratio_lower = 0.85; + m_area_ratio_upper = 1.1; + } + + static const GigantamaxMatcher& instance(){ + static GigantamaxMatcher matcher; + return matcher; + } +}; + + +GigantamaxDetector::GigantamaxDetector( + Color color, + VideoOverlay* overlay, + const ImageFloatBox& box +) + : m_color(color) + , m_overlay(overlay) + , m_box(box) +{} +void GigantamaxDetector::make_overlays(VideoOverlaySet& items) const{ + items.add(m_color, m_box); +} +bool GigantamaxDetector::detect(const ImageViewRGB32& screen){ + const double screen_rel_size = (screen.height() / 1080.0); + const double screen_rel_size_2 = screen_rel_size * screen_rel_size; + + const double min_area_1080p = 400; + const double rmsd_threshold = 80; + const size_t min_area = size_t(screen_rel_size_2 * min_area_1080p); + + const std::vector> FILTERS = { + {0xffc42058, 0xffffdceb}, + }; + + const bool found = match_template_by_waterfill( + screen.size(), + extract_box_reference(screen, m_box), + GigantamaxMatcher::instance(), + FILTERS, + {min_area, SIZE_MAX}, + rmsd_threshold, + [&](Kernels::Waterfill::WaterfillObject& object) -> bool { + m_last_detected = translate_to_parent(screen, m_box, object); + return true; + } + ); + + if (m_overlay){ + if (found){ + m_last_detected_box.emplace(*m_overlay, m_last_detected, COLOR_GREEN); + }else{ + m_last_detected_box.reset(); + } + } + + return found; +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_GigantamaxDetector.h b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_GigantamaxDetector.h new file mode 100644 index 0000000000..313e9a00cc --- /dev/null +++ b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_GigantamaxDetector.h @@ -0,0 +1,57 @@ +/* Gigantamax Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonHome_GigantamaxDetector_H +#define PokemonAutomation_PokemonHome_GigantamaxDetector_H + +#include +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/VisualDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonHome{ + + +class GigantamaxDetector : public StaticScreenDetector{ +public: + GigantamaxDetector( + Color color, + VideoOverlay* overlay, + const ImageFloatBox& box + ); + + const ImageFloatBox& last_detected() const{ return m_last_detected; } + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + const Color m_color; + VideoOverlay* m_overlay; + const ImageFloatBox m_box; + + ImageFloatBox m_last_detected; + std::optional m_last_detected_box; +}; +class GigantamaxWatcher : public DetectorToFinder{ +public: + GigantamaxWatcher( + Color color, + VideoOverlay* overlay, + const ImageFloatBox& box, + std::chrono::milliseconds hold_duration = std::chrono::milliseconds(250) + ) + : DetectorToFinder("GigantamaxWatcher", hold_duration, color, overlay, box) + {} +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp index ec2e679715..0912520646 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp @@ -51,6 +51,7 @@ language #include "PokemonHome/Inference/PokemonHome_BoxGenderDetector.h" #include "PokemonHome/Inference/PokemonHome_BallReader.h" #include "PokemonHome_BoxSorter.h" +#include "PokemonHome/Inference/PokemonHome_GigantamaxDetector.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -391,9 +392,7 @@ void read_summary_screen( ImageFloatBox national_dex_number_box(0.448, 0.245, 0.049, 0.04); //pokemon national dex number pos ImageFloatBox shiny_symbol_box(0.702, 0.09, 0.04, 0.06); // shiny symbol pos - // TODO: gmax symbol is at the same location as Tera type symbol! Need better detection to tell apart - // gmax symbol and tera types - ImageFloatBox gmax_symbol_box(0.463, 0.09, 0.04, 0.06); // gmax symbol pos + ImageFloatBox gmax_tera_symbol_box(0.463, 0.09, 0.04, 0.06); // gmax OR tera symbol pos ImageFloatBox origin_symbol_box(0.623, 0.095, 0.033, 0.05); // origin symbol pos ImageFloatBox pokemon_box(0.69, 0.18, 0.28, 0.46); // pokemon render pos ImageFloatBox level_box(0.546, 0.099, 0.044, 0.041); // Level @@ -407,7 +406,7 @@ void read_summary_screen( video_overlay_set.add(COLOR_WHITE, national_dex_number_box); video_overlay_set.add(COLOR_BLUE, shiny_symbol_box); - video_overlay_set.add(COLOR_RED, gmax_symbol_box); + video_overlay_set.add(COLOR_RED, gmax_tera_symbol_box); video_overlay_set.add(COLOR_RED, alpha_box); video_overlay_set.add(COLOR_DARKGREEN, origin_symbol_box); video_overlay_set.add(COLOR_DARK_BLUE, pokemon_box); @@ -442,10 +441,28 @@ void read_summary_screen( cur_pokemon_info.shiny = is_shiny; env.console.log("Shiny detection stddev:" + std::to_string(shiny_stddev_value) + " is shiny:" + std::to_string(is_shiny)); - const int gmax_stddev_value = (int)image_stddev(extract_box_reference(screen, gmax_symbol_box)).sum(); - const bool is_gmax = gmax_stddev_value > 30; + GigantamaxDetector gmax_detector(COLOR_RED, &env.console.overlay(), gmax_tera_symbol_box); + bool is_gmax = gmax_detector.detect(screen); cur_pokemon_info.gmax = is_gmax; - env.console.log("Gmax detection stddev:" + std::to_string(gmax_stddev_value) + " is gmax:" + std::to_string(is_gmax)); + + auto [tera_type_primary, tera_type_secondary] = read_pokemon_types(screen, gmax_tera_symbol_box, PokemonTypeGeneration::GEN9); + cur_pokemon_info.tera_type = tera_type_primary; + + if (is_gmax && tera_type_primary != PokemonType::NONE){ + env.console.log( + "Gigantamax form detected with tera type: " + + POKEMON_TYPE_SLUGS().get_string(tera_type_primary) + + " . One of these is a false positive!", COLOR_RED + ); + } + + if (tera_type_secondary != PokemonType::NONE){ + env.console.log( + "Secondary tera type detected: " + + POKEMON_TYPE_SLUGS().get_string(tera_type_secondary) + + " . This is a false positive!", COLOR_RED + ); + } const int alpha_stddev_value = (int)image_stddev(extract_box_reference(screen, alpha_box)).sum(); const bool is_alpha = alpha_stddev_value > 40; @@ -465,10 +482,10 @@ void read_summary_screen( } cur_pokemon_info.ot_id = ot_id; - auto [primaryType, secondaryType] = read_pokemon_types(screen, type_box, PokemonTypeGeneration::GEN9); + auto [primary_type, secondary_type] = read_pokemon_types(screen, type_box, PokemonTypeGeneration::GEN9); - cur_pokemon_info.primaryType = primaryType; - cur_pokemon_info.secondaryType = secondaryType; + cur_pokemon_info.primary_type = primary_type; + cur_pokemon_info.secondary_type = secondary_type; env.add_overlay_log(create_overlay_info(cur_pokemon_info)); video_overlay_set.clear(); @@ -488,6 +505,21 @@ void read_summary_screen( void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ StartProgramChecks::check_performance_class_wired_or_wireless(context); + VideoOverlay& overlay = env.console.overlay(); + ImageFloatBox gmax_test_box(0.459, 0.084, 0.043, 0.067); + GigantamaxDetector detector(COLOR_RED, &overlay, gmax_test_box); + for (size_t i = 0; i < 10; i++){ + VideoSnapshot screen = env.console.video().snapshot(); + if (detector.detect(screen)){ + env.console.log("Gmax symbol detected", COLOR_CYAN); + } else{ + env.console.log("Gmax symbol not detected", COLOR_RED); + } + pbf_wait(context, 2000ms); + context.wait_for_all_requests(); + } + + const std::vector sort_preferences = SORT_TABLE.preferences(); if (sort_preferences.empty()){ throw UserSetupError(env.console, "At least one sorting method selection needs to be made!"); diff --git a/SerialPrograms/Source/PokemonLZA/Programs/PokemonLZA_BoxSorter.cpp b/SerialPrograms/Source/PokemonLZA/Programs/PokemonLZA_BoxSorter.cpp index 54ded44d1b..a1c608e869 100644 --- a/SerialPrograms/Source/PokemonLZA/Programs/PokemonLZA_BoxSorter.cpp +++ b/SerialPrograms/Source/PokemonLZA/Programs/PokemonLZA_BoxSorter.cpp @@ -320,7 +320,7 @@ void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContex name_slug = LUMIOSE_DEX_SLUGS()[dex_number-1]; } - auto [primaryType, secondaryType] = read_pokemon_types(screen, ImageFloatBox(0.467, 0.256, 0.110, 0.041), PokemonTypeGeneration::GEN9); + auto [primary_type, secondary_type] = read_pokemon_types(screen, ImageFloatBox(0.467, 0.256, 0.110, 0.041), PokemonTypeGeneration::GEN9); boxes_data.push_back( CollectedPokemonInfo{ .preferences = &sort_preferences, @@ -328,8 +328,8 @@ void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContex .name_slug = name_slug, .shiny = shiny_detector.detect(screen), .alpha = alpha_detector.detect(screen), - .primaryType = primaryType, - .secondaryType = secondaryType, + .primary_type = primary_type, + .secondary_type = secondary_type, } ); ss << "\u2705 " ; // checkbox diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index ca35f885a2..61444756b2 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1551,6 +1551,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonHome/Inference/PokemonHome_BoxGenderDetector.h Source/PokemonHome/Inference/PokemonHome_ButtonDetector.cpp Source/PokemonHome/Inference/PokemonHome_ButtonDetector.h + Source/PokemonHome/Inference/PokemonHome_GigantamaxDetector.cpp + Source/PokemonHome/Inference/PokemonHome_GigantamaxDetector.h Source/PokemonHome/PokemonHome_Panels.cpp Source/PokemonHome/PokemonHome_Panels.h Source/PokemonHome/PokemonHome_Settings.cpp From ec81ac2e17a3770863fd5fd1900a5766e35306d0 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Sun, 26 Apr 2026 08:44:58 -0500 Subject: [PATCH 2/7] cleanup --- .../Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp index 0912520646..b494818754 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp @@ -50,8 +50,8 @@ language #include "PokemonHome/Inference/PokemonHome_ButtonDetector.h" #include "PokemonHome/Inference/PokemonHome_BoxGenderDetector.h" #include "PokemonHome/Inference/PokemonHome_BallReader.h" -#include "PokemonHome_BoxSorter.h" #include "PokemonHome/Inference/PokemonHome_GigantamaxDetector.h" +#include "PokemonHome_BoxSorter.h" namespace PokemonAutomation{ namespace NintendoSwitch{ From e854c1a15d65b2d684806cd96f6624fd341e5330 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Sun, 26 Apr 2026 14:47:18 -0500 Subject: [PATCH 3/7] Remove test code --- .../Programs/PokemonHome_BoxSorter.cpp | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp index b494818754..9ae1fd8826 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp @@ -505,21 +505,6 @@ void read_summary_screen( void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ StartProgramChecks::check_performance_class_wired_or_wireless(context); - VideoOverlay& overlay = env.console.overlay(); - ImageFloatBox gmax_test_box(0.459, 0.084, 0.043, 0.067); - GigantamaxDetector detector(COLOR_RED, &overlay, gmax_test_box); - for (size_t i = 0; i < 10; i++){ - VideoSnapshot screen = env.console.video().snapshot(); - if (detector.detect(screen)){ - env.console.log("Gmax symbol detected", COLOR_CYAN); - } else{ - env.console.log("Gmax symbol not detected", COLOR_RED); - } - pbf_wait(context, 2000ms); - context.wait_for_all_requests(); - } - - const std::vector sort_preferences = SORT_TABLE.preferences(); if (sort_preferences.empty()){ throw UserSetupError(env.console, "At least one sorting method selection needs to be made!"); From 3ec08bd570a0e487280650ee66405879b218c66a Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Mon, 27 Apr 2026 13:15:58 -0500 Subject: [PATCH 4/7] Add tera types --- .../Pokemon/Pokemon_CollectedPokemonInfo.cpp | 2 +- .../Pokemon/Pokemon_CollectedPokemonInfo.h | 2 +- .../Source/Pokemon/Pokemon_Types.cpp | 26 ++++++++++++++++++- SerialPrograms/Source/Pokemon/Pokemon_Types.h | 24 +++++++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp index ae02e69b8e..f1e8a8bbae 100644 --- a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp +++ b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp @@ -111,7 +111,7 @@ std::ostream& operator<<(std::ostream& os, const std::optionalot_id << " "; os << "primaryType:" << POKEMON_TYPE_SLUGS().get_string(pokemon->primary_type) << " "; os << "secondaryType:" << POKEMON_TYPE_SLUGS().get_string(pokemon->secondary_type) << " "; - os << "teraType:" << POKEMON_TYPE_SLUGS().get_string(pokemon->tera_type) << " "; + os << "teraType:" << POKEMON_TERA_TYPE_SLUGS().get_string(pokemon->tera_type) << " "; os << ")"; }else{ os << "(empty)"; diff --git a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h index c71008c439..8e0935e56d 100644 --- a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h +++ b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h @@ -34,7 +34,7 @@ struct CollectedPokemonInfo{ uint32_t ot_id = 0; // original trainer ID PokemonType primary_type = PokemonType::NONE; PokemonType secondary_type = PokemonType::NONE; - PokemonType tera_type = PokemonType::NONE; + PokemonTeraType tera_type = PokemonTeraType::NONE; }; bool operator==(const CollectedPokemonInfo& lhs, const CollectedPokemonInfo& rhs); diff --git a/SerialPrograms/Source/Pokemon/Pokemon_Types.cpp b/SerialPrograms/Source/Pokemon/Pokemon_Types.cpp index 460616a381..fbbcd12e52 100644 --- a/SerialPrograms/Source/Pokemon/Pokemon_Types.cpp +++ b/SerialPrograms/Source/Pokemon/Pokemon_Types.cpp @@ -9,7 +9,31 @@ namespace PokemonAutomation{ namespace Pokemon{ - +const EnumStringMap& POKEMON_TERA_TYPE_SLUGS(){ + static EnumStringMap database{ + {PokemonTeraType::BUG, "bug"}, + {PokemonTeraType::DARK, "dark"}, + {PokemonTeraType::DRAGON, "dragon"}, + {PokemonTeraType::ELECTRIC, "electric"}, + {PokemonTeraType::FAIRY, "fairy"}, + {PokemonTeraType::FIGHTING, "fighting"}, + {PokemonTeraType::FIRE, "fire"}, + {PokemonTeraType::FLYING, "flying"}, + {PokemonTeraType::GHOST, "ghost"}, + {PokemonTeraType::GRASS, "grass"}, + {PokemonTeraType::GROUND, "ground"}, + {PokemonTeraType::ICE, "ice"}, + {PokemonTeraType::NONE, "none"}, + {PokemonTeraType::NORMAL, "normal"}, + {PokemonTeraType::POISON, "poison"}, + {PokemonTeraType::PSYCHIC, "psychic"}, + {PokemonTeraType::ROCK, "rock"}, + {PokemonTeraType::STEEL, "steel"}, + {PokemonTeraType::STELLAR, "stellar"}, + {PokemonTeraType::WATER, "water"} + }; + return database; +} const EnumStringMap& POKEMON_TYPE_SLUGS(){ static EnumStringMap database{ diff --git a/SerialPrograms/Source/Pokemon/Pokemon_Types.h b/SerialPrograms/Source/Pokemon/Pokemon_Types.h index 86417a2ea8..04ae64c671 100644 --- a/SerialPrograms/Source/Pokemon/Pokemon_Types.h +++ b/SerialPrograms/Source/Pokemon/Pokemon_Types.h @@ -21,6 +21,30 @@ enum class MoveCategory{ UNKNOWN, }; +enum class PokemonTeraType{ + BUG, + DARK, + DRAGON, + ELECTRIC, + FAIRY, + FIGHTING, + FIRE, + FLYING, + GHOST, + GRASS, + GROUND, + ICE, + NONE, + NORMAL, + POISON, + PSYCHIC, + ROCK, + STEEL, + STELLAR, + WATER +}; +const EnumStringMap& POKEMON_TERA_TYPE_SLUGS(); + enum class PokemonType{ NONE, NORMAL, From 05f399a1988a0266ca3b4b9a27fae711b82c6537 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Mon, 27 Apr 2026 22:11:35 -0500 Subject: [PATCH 5/7] Read tera type --- .../Pokemon/Inference/Pokemon_TypeReader.cpp | 224 ++++++++++++++++-- .../Pokemon/Inference/Pokemon_TypeReader.h | 13 + .../Pokemon/Pokemon_CollectedPokemonInfo.cpp | 2 +- .../Programs/PokemonHome_BoxSorter.cpp | 20 +- 4 files changed, 220 insertions(+), 39 deletions(-) diff --git a/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.cpp b/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.cpp index a337613d80..cf0fdae287 100644 --- a/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.cpp +++ b/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.cpp @@ -22,24 +22,12 @@ namespace Pokemon { using namespace Kernels; using namespace Kernels::Waterfill; - class TypeSprite { public: - TypeSprite(const std::string& slug, PokemonTypeGeneration generation) + TypeSprite(const std::string& slug, const std::string& resource_location) : m_slug(slug) { - ImageRGB32 sprite; - switch (generation) - { - case PokemonAutomation::Pokemon::PokemonTypeGeneration::GEN8: - sprite = ImageRGB32(RESOURCE_PATH() + "Pokemon/Types/Gen8/" + slug + ".png"); - break; - case PokemonAutomation::Pokemon::PokemonTypeGeneration::GEN9: - sprite = ImageRGB32(RESOURCE_PATH() + "Pokemon/Types/Gen9/" + slug + ".png"); - break; - default: - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Invalid enum."); - } + ImageRGB32 sprite(RESOURCE_PATH() + resource_location); sprite = filter_rgb32_range( sprite, @@ -85,7 +73,7 @@ struct Gen8TypeSpriteDatabase { if (item.first == PokemonType::NONE){ continue; } - m_type_map.emplace(item.first, TypeSprite(item.second, PokemonTypeGeneration::GEN8)); + m_type_map.emplace(item.first, TypeSprite(item.second, "Pokemon/Types/Gen8/" + item.second + ".png")); } } }; @@ -103,7 +91,25 @@ struct Gen9TypeSpriteDatabase { if (item.first == PokemonType::NONE){ continue; } - m_type_map.emplace(item.first, TypeSprite(item.second, PokemonTypeGeneration::GEN9)); + m_type_map.emplace(item.first, TypeSprite(item.second, "Pokemon/Types/Gen9/" + item.second + ".png")); + } + } +}; + +struct TeraTypeSpriteDatabase { + std::map m_type_map; + + static TeraTypeSpriteDatabase& instance(){ + static TeraTypeSpriteDatabase data; + return data; + } + + TeraTypeSpriteDatabase(){ + for (const auto& item : POKEMON_TERA_TYPE_SLUGS()){ + if (item.first == PokemonTeraType::NONE){ + continue; + } + m_type_map.emplace(item.first, TypeSprite(item.second, "Pokemon/Types/Tera/" + item.second + ".png")); } } }; @@ -132,26 +138,72 @@ size_t distance_sqr(const ImagePixelBox& a, const ImagePixelBox& b){ return dist_x * dist_x + dist_y * dist_y; } -std::pair match_type_symbol(const ImageViewRGB32& image, PokemonTypeGeneration generation){ +bool image_validation(const ImageViewRGB32& image){ size_t width = image.width(); size_t height = image.height(); if (width * height < 100){ - return { 1.0, PokemonType::NONE }; + return false; } if (width > 2 * height){ - return { 1.0, PokemonType::NONE }; + return false; } if (height > 2 * width){ - return { 1.0, PokemonType::NONE }; + return false; } ImageStats stats = image_stats(image); if (stats.stddev.sum() < 50){ // if (print){ // cout << "stats.stddev.sum() = " << stats.stddev.sum() << endl; // } + return false; + } + + return true; +} + +std::pair match_tera_type_symbol(const ImageViewRGB32& image){ + + if (!image_validation(image)){ + return { 1.0, PokemonTeraType::NONE }; + } + + size_t width = image.width(); + size_t height = image.height(); + + double aspect_ratio = (double)width / height; + + const std::map* type_sprite_map = &TeraTypeSpriteDatabase::instance().m_type_map; + + double best_score = 0.45; + PokemonTeraType best_type = PokemonTeraType::NONE; + for (const auto& item : *type_sprite_map){ + double expected_aspect_ratio = item.second.aspect_ratio(); + double ratio = aspect_ratio / expected_aspect_ratio; + + if (std::abs(ratio - 1) > 0.2){ + continue; + } + + double rmsd_alpha = item.second.matcher().diff(image); + + if (best_score > rmsd_alpha){ + best_score = rmsd_alpha; + best_type = item.first; + } + } + + return { best_score, best_type }; +} + +std::pair match_type_symbol(const ImageViewRGB32& image, PokemonTypeGeneration generation){ + + if (!image_validation(image)){ return { 1.0, PokemonType::NONE }; } + size_t width = image.width(); + size_t height = image.height(); + double aspect_ratio = (double)width / height; // static int c = 0; @@ -226,6 +278,73 @@ std::pair match_type_symbol(const ImageViewRGB32& image, Po return { best_score, best_type }; } +void find_tera_type_symbol_candidates( + std::multimap>& candidates, + const ImageViewPlanar32& original_screen, + const ImageViewRGB32& image, + PackedBinaryMatrix& matrix, + double max_area_ratio +){ + size_t max_area = (size_t)(image.width() * image.height() * max_area_ratio); + std::vector objects = find_objects_inplace( + matrix, + (size_t)(20. * original_screen.total_pixels() / (1920 * 1080)) + ); + + std::map objmap; + for (size_t c = 0; c < objects.size(); c++){ + if (objects[c].area > max_area){ + continue; + } + objmap[c] = objects[c]; + } + + // Merge nearby objects. + bool changed; + do{ + changed = false; + for (auto iter0 = objmap.begin(); iter0 != objmap.end(); ++iter0){ + for (auto iter1 = objmap.begin(); iter1 != objmap.end();){ + if (iter0->first >= iter1->first){ + ++iter1; + continue; + } + const WaterfillObject& obj0 = iter0->second; + const WaterfillObject& obj1 = iter1->second; + size_t distance = distance_sqr( + ImagePixelBox(obj0.min_x, obj0.min_y, obj0.max_x, obj0.max_y), + ImagePixelBox(obj1.min_x, obj1.min_y, obj1.max_x, obj1.max_y) + ); + if (distance < 5 * 5){ + iter0->second.merge_assume_no_overlap(iter1->second); + iter1 = objmap.erase(iter1); + changed = true; + } else{ + ++iter1; + } + } + } + } + while (changed); + + // Identify objects. + for (const auto& item : objmap){ + ImageViewRGB32 img = extract_box_reference(image, item.second); + + std::pair result = match_tera_type_symbol(img); + if (result.second != PokemonTeraType::NONE){ + const WaterfillObject& obj = item.second; + candidates.emplace( + result.first, + std::pair( + result.second, + ImagePixelBox(obj.min_x, obj.min_y, obj.max_x, obj.max_y) + ) + ); + } + } +} + void find_type_symbol_candidates( std::multimap>& candidates, const ImageViewPlanar32& original_screen, @@ -309,6 +428,51 @@ void find_type_symbol_candidates( // cout << "candidates = " << candidates.size() << endl; } +std::multimap> find_tera_type_symbols( + const ImageViewPlanar32& original_screen, + const ImageViewRGB32& image, + double max_area_ratio +){ + std::multimap> candidates; + + { + std::vector matrices = compress_rgb32_to_binary_range( + image, + { + {0xff808060, 0xffffffff}, + {0xffa0a060, 0xffffffff}, + {0xff606060, 0xffffffff}, + {0xff707070, 0xffffffff}, + {0xff808080, 0xffffffff}, + {0xff909090, 0xffffffff}, + {0xffa0a0a0, 0xffffffff}, + {0xffb0b0b0, 0xffffffff}, + {0xffc0c0c0, 0xffffffff}, + {0xffd0d0d0, 0xffffffff}, + {0xffe0e0e0, 0xffffffff}, + } + ); + for (PackedBinaryMatrix& matrix : matrices){ + find_tera_type_symbol_candidates(candidates, original_screen, image, matrix, max_area_ratio); + } + } + + std::multimap> filtered; + for (const auto& candidate : candidates){ + bool is_dupe = false; + for (const auto& item : filtered){ + if (distance_sqr(candidate.second.second, item.second.second) == 0){ + is_dupe = true; + break; + } + } + if (!is_dupe){ + filtered.emplace(candidate); + } + } + + return filtered; +} std::multimap> find_type_symbols( const ImageViewPlanar32& original_screen, @@ -375,6 +539,26 @@ std::multimap> find_type_symbols( return filtered; } +PokemonTeraType read_pokemon_tera_type( + const ImageViewRGB32& original_screen, + const ImageFloatBox& box +){ + + ImageViewRGB32 image = extract_box_reference(original_screen, box); + + std::multimap> filtered = find_tera_type_symbols( + original_screen, + image, + 0.20 + ); + + if (filtered.empty()){ + return PokemonTeraType::NONE; + } + + return filtered.begin()->second.first; +} + std::pair read_pokemon_types( const ImageViewRGB32& original_screen, const ImageFloatBox& box, diff --git a/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.h b/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.h index bdc48e00e1..8330c6f0e1 100644 --- a/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.h +++ b/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.h @@ -23,6 +23,13 @@ enum class PokemonTypeGeneration{ GEN9, }; +// Find all tera type symbols inside the image. +std::multimap> find_tera_type_symbols( + const ImageViewPlanar32& original_screen, + const ImageViewRGB32& image, + double max_area_ratio +); + // Find all type symbols inside the image. std::multimap> find_type_symbols( const ImageViewPlanar32& original_screen, @@ -31,6 +38,12 @@ std::multimap> find_type_symbols( PokemonTypeGeneration generation ); +// Reads the tera type of a Pokemon. +PokemonTeraType read_pokemon_tera_type( + const ImageViewRGB32& original_screen, + const ImageFloatBox& box +); + // Reads the types of a Pokemon. Second type will be PokemonType::NONE if the Pokemon is single-type std::pair read_pokemon_types( const ImageViewRGB32& original_screen, diff --git a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp index f1e8a8bbae..e225914298 100644 --- a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp +++ b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp @@ -164,7 +164,7 @@ void save_boxes_data_to_json(const std::vectorot_id; pokemon["primary_type"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->primary_type); pokemon["secondary_type"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->secondary_type); - pokemon["tera_type"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->tera_type); + pokemon["tera_type"] = POKEMON_TERA_TYPE_SLUGS().get_string(current_pokemon->tera_type); } pokemon_data.push_back(std::move(pokemon)); } diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp index 9ae1fd8826..3b14530b05 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp @@ -445,24 +445,8 @@ void read_summary_screen( bool is_gmax = gmax_detector.detect(screen); cur_pokemon_info.gmax = is_gmax; - auto [tera_type_primary, tera_type_secondary] = read_pokemon_types(screen, gmax_tera_symbol_box, PokemonTypeGeneration::GEN9); - cur_pokemon_info.tera_type = tera_type_primary; - - if (is_gmax && tera_type_primary != PokemonType::NONE){ - env.console.log( - "Gigantamax form detected with tera type: " - + POKEMON_TYPE_SLUGS().get_string(tera_type_primary) - + " . One of these is a false positive!", COLOR_RED - ); - } - - if (tera_type_secondary != PokemonType::NONE){ - env.console.log( - "Secondary tera type detected: " - + POKEMON_TYPE_SLUGS().get_string(tera_type_secondary) - + " . This is a false positive!", COLOR_RED - ); - } + PokemonTeraType tera_type = read_pokemon_tera_type(screen, gmax_tera_symbol_box); + cur_pokemon_info.tera_type = tera_type; const int alpha_stddev_value = (int)image_stddev(extract_box_reference(screen, alpha_box)).sum(); const bool is_alpha = alpha_stddev_value > 40; From 8f32272080544bd65d19cf0e990a06ca6d0b5e6c Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Tue, 28 Apr 2026 07:55:30 -0500 Subject: [PATCH 6/7] Move tera type detection to Home folders --- .../Pokemon/Inference/Pokemon_TypeReader.cpp | 185 ----------- .../Pokemon/Inference/Pokemon_TypeReader.h | 13 - .../Inference/PokemonHome_TeraTypeReader.cpp | 289 ++++++++++++++++++ .../Inference/PokemonHome_TeraTypeReader.h | 34 +++ .../Programs/PokemonHome_BoxSorter.cpp | 1 + SerialPrograms/cmake/SourceFiles.cmake | 2 + 6 files changed, 326 insertions(+), 198 deletions(-) create mode 100644 SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TeraTypeReader.cpp create mode 100644 SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TeraTypeReader.h diff --git a/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.cpp b/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.cpp index cf0fdae287..0c68f8a579 100644 --- a/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.cpp +++ b/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.cpp @@ -96,24 +96,6 @@ struct Gen9TypeSpriteDatabase { } }; -struct TeraTypeSpriteDatabase { - std::map m_type_map; - - static TeraTypeSpriteDatabase& instance(){ - static TeraTypeSpriteDatabase data; - return data; - } - - TeraTypeSpriteDatabase(){ - for (const auto& item : POKEMON_TERA_TYPE_SLUGS()){ - if (item.first == PokemonTeraType::NONE){ - continue; - } - m_type_map.emplace(item.first, TypeSprite(item.second, "Pokemon/Types/Tera/" + item.second + ".png")); - } - } -}; - size_t distance_sqr(const ImagePixelBox& a, const ImagePixelBox& b){ bool overlap_x = a.min_x <= b.max_x && b.min_x <= a.max_x; bool overlap_y = a.min_y <= b.max_y && b.min_y <= a.max_y; @@ -161,40 +143,6 @@ bool image_validation(const ImageViewRGB32& image){ return true; } -std::pair match_tera_type_symbol(const ImageViewRGB32& image){ - - if (!image_validation(image)){ - return { 1.0, PokemonTeraType::NONE }; - } - - size_t width = image.width(); - size_t height = image.height(); - - double aspect_ratio = (double)width / height; - - const std::map* type_sprite_map = &TeraTypeSpriteDatabase::instance().m_type_map; - - double best_score = 0.45; - PokemonTeraType best_type = PokemonTeraType::NONE; - for (const auto& item : *type_sprite_map){ - double expected_aspect_ratio = item.second.aspect_ratio(); - double ratio = aspect_ratio / expected_aspect_ratio; - - if (std::abs(ratio - 1) > 0.2){ - continue; - } - - double rmsd_alpha = item.second.matcher().diff(image); - - if (best_score > rmsd_alpha){ - best_score = rmsd_alpha; - best_type = item.first; - } - } - - return { best_score, best_type }; -} - std::pair match_type_symbol(const ImageViewRGB32& image, PokemonTypeGeneration generation){ if (!image_validation(image)){ @@ -278,73 +226,6 @@ std::pair match_type_symbol(const ImageViewRGB32& image, Po return { best_score, best_type }; } -void find_tera_type_symbol_candidates( - std::multimap>& candidates, - const ImageViewPlanar32& original_screen, - const ImageViewRGB32& image, - PackedBinaryMatrix& matrix, - double max_area_ratio -){ - size_t max_area = (size_t)(image.width() * image.height() * max_area_ratio); - std::vector objects = find_objects_inplace( - matrix, - (size_t)(20. * original_screen.total_pixels() / (1920 * 1080)) - ); - - std::map objmap; - for (size_t c = 0; c < objects.size(); c++){ - if (objects[c].area > max_area){ - continue; - } - objmap[c] = objects[c]; - } - - // Merge nearby objects. - bool changed; - do{ - changed = false; - for (auto iter0 = objmap.begin(); iter0 != objmap.end(); ++iter0){ - for (auto iter1 = objmap.begin(); iter1 != objmap.end();){ - if (iter0->first >= iter1->first){ - ++iter1; - continue; - } - const WaterfillObject& obj0 = iter0->second; - const WaterfillObject& obj1 = iter1->second; - size_t distance = distance_sqr( - ImagePixelBox(obj0.min_x, obj0.min_y, obj0.max_x, obj0.max_y), - ImagePixelBox(obj1.min_x, obj1.min_y, obj1.max_x, obj1.max_y) - ); - if (distance < 5 * 5){ - iter0->second.merge_assume_no_overlap(iter1->second); - iter1 = objmap.erase(iter1); - changed = true; - } else{ - ++iter1; - } - } - } - } - while (changed); - - // Identify objects. - for (const auto& item : objmap){ - ImageViewRGB32 img = extract_box_reference(image, item.second); - - std::pair result = match_tera_type_symbol(img); - if (result.second != PokemonTeraType::NONE){ - const WaterfillObject& obj = item.second; - candidates.emplace( - result.first, - std::pair( - result.second, - ImagePixelBox(obj.min_x, obj.min_y, obj.max_x, obj.max_y) - ) - ); - } - } -} - void find_type_symbol_candidates( std::multimap>& candidates, const ImageViewPlanar32& original_screen, @@ -428,52 +309,6 @@ void find_type_symbol_candidates( // cout << "candidates = " << candidates.size() << endl; } -std::multimap> find_tera_type_symbols( - const ImageViewPlanar32& original_screen, - const ImageViewRGB32& image, - double max_area_ratio -){ - std::multimap> candidates; - - { - std::vector matrices = compress_rgb32_to_binary_range( - image, - { - {0xff808060, 0xffffffff}, - {0xffa0a060, 0xffffffff}, - {0xff606060, 0xffffffff}, - {0xff707070, 0xffffffff}, - {0xff808080, 0xffffffff}, - {0xff909090, 0xffffffff}, - {0xffa0a0a0, 0xffffffff}, - {0xffb0b0b0, 0xffffffff}, - {0xffc0c0c0, 0xffffffff}, - {0xffd0d0d0, 0xffffffff}, - {0xffe0e0e0, 0xffffffff}, - } - ); - for (PackedBinaryMatrix& matrix : matrices){ - find_tera_type_symbol_candidates(candidates, original_screen, image, matrix, max_area_ratio); - } - } - - std::multimap> filtered; - for (const auto& candidate : candidates){ - bool is_dupe = false; - for (const auto& item : filtered){ - if (distance_sqr(candidate.second.second, item.second.second) == 0){ - is_dupe = true; - break; - } - } - if (!is_dupe){ - filtered.emplace(candidate); - } - } - - return filtered; -} - std::multimap> find_type_symbols( const ImageViewPlanar32& original_screen, const ImageViewRGB32& image, @@ -539,26 +374,6 @@ std::multimap> find_type_symbols( return filtered; } -PokemonTeraType read_pokemon_tera_type( - const ImageViewRGB32& original_screen, - const ImageFloatBox& box -){ - - ImageViewRGB32 image = extract_box_reference(original_screen, box); - - std::multimap> filtered = find_tera_type_symbols( - original_screen, - image, - 0.20 - ); - - if (filtered.empty()){ - return PokemonTeraType::NONE; - } - - return filtered.begin()->second.first; -} - std::pair read_pokemon_types( const ImageViewRGB32& original_screen, const ImageFloatBox& box, diff --git a/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.h b/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.h index 8330c6f0e1..bdc48e00e1 100644 --- a/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.h +++ b/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.h @@ -23,13 +23,6 @@ enum class PokemonTypeGeneration{ GEN9, }; -// Find all tera type symbols inside the image. -std::multimap> find_tera_type_symbols( - const ImageViewPlanar32& original_screen, - const ImageViewRGB32& image, - double max_area_ratio -); - // Find all type symbols inside the image. std::multimap> find_type_symbols( const ImageViewPlanar32& original_screen, @@ -38,12 +31,6 @@ std::multimap> find_type_symbols( PokemonTypeGeneration generation ); -// Reads the tera type of a Pokemon. -PokemonTeraType read_pokemon_tera_type( - const ImageViewRGB32& original_screen, - const ImageFloatBox& box -); - // Reads the types of a Pokemon. Second type will be PokemonType::NONE if the Pokemon is single-type std::pair read_pokemon_types( const ImageViewRGB32& original_screen, diff --git a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TeraTypeReader.cpp b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TeraTypeReader.cpp new file mode 100644 index 0000000000..69bbbf4483 --- /dev/null +++ b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TeraTypeReader.cpp @@ -0,0 +1,289 @@ +/* Tera Type Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/Globals.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonTools/ImageMatch/ExactImageMatcher.h" +#include "CommonTools/Images/ImageFilter.h" +#include "CommonTools/Images/BinaryImage_FilterRgb32.h" +#include "Kernels/Waterfill/Kernels_Waterfill.h" +#include "PokemonHome_TeraTypeReader.h" + +namespace PokemonAutomation { +namespace Pokemon { + +using namespace Kernels; +using namespace Kernels::Waterfill; + +namespace { + +class TypeSprite { +public: + TypeSprite(const std::string& slug, const std::string& resource_location) + : m_slug(slug) + { + ImageRGB32 sprite(RESOURCE_PATH() + resource_location); + + sprite = filter_rgb32_range( + sprite, + 0xff000000, 0xffffffff, + Color(0), false + ); + + PackedBinaryMatrix matrix = compress_rgb32_to_binary_min(sprite, 224, 224, 224); + std::vector objects = find_objects_inplace(matrix, 10); + + WaterfillObject object; + for (const WaterfillObject& item : objects){ + object.merge_assume_no_overlap(item); + } + + m_aspect_ratio = object.aspect_ratio(); + m_matcher = std::make_unique( + sprite.sub_image(object.min_x, object.min_y, object.width(), object.height()).copy(), + ImageMatch::WeightedExactImageMatcher::InverseStddevWeight{ 1, 64 } + ); + } + + const std::string& slug() const { return m_slug; } + double aspect_ratio() const { return m_aspect_ratio; } + const ImageMatch::WeightedExactImageMatcher& matcher() const { return *m_matcher; } + +private: + std::string m_slug; + double m_aspect_ratio; + std::unique_ptr m_matcher; +}; + +struct TeraTypeSpriteDatabase { + std::map m_type_map; + + static TeraTypeSpriteDatabase& instance(){ + static TeraTypeSpriteDatabase data; + return data; + } + + TeraTypeSpriteDatabase(){ + for (const auto& item : POKEMON_TERA_TYPE_SLUGS()){ + if (item.first == PokemonTeraType::NONE){ + continue; + } + m_type_map.emplace(item.first, TypeSprite(item.second, "Pokemon/Types/Tera/" + item.second + ".png")); + } + } +}; + +size_t distance_sqr(const ImagePixelBox& a, const ImagePixelBox& b){ + bool overlap_x = a.min_x <= b.max_x && b.min_x <= a.max_x; + bool overlap_y = a.min_y <= b.max_y && b.min_y <= a.max_y; + if (overlap_x && overlap_y){ + return 0; + } + + size_t dist_x = 0; + if (!overlap_x){ + dist_x = a.max_x < b.min_x + ? b.min_x - a.max_x + : a.min_x - b.max_x; + } + + size_t dist_y = 0; + if (!overlap_y){ + dist_y = a.max_y < b.min_y + ? b.min_y - a.max_y + : a.min_y - b.max_y; + } + + return dist_x * dist_x + dist_y * dist_y; +} + +bool image_validation(const ImageViewRGB32& image){ + size_t width = image.width(); + size_t height = image.height(); + if (width * height < 100){ + return false; + } + if (width > 2 * height){ + return false; + } + if (height > 2 * width){ + return false; + } + ImageStats stats = image_stats(image); + if (stats.stddev.sum() < 50){ + return false; + } + + return true; +} + +std::pair match_tera_type_symbol(const ImageViewRGB32& image){ + if (!image_validation(image)){ + return { 1.0, PokemonTeraType::NONE }; + } + + size_t width = image.width(); + size_t height = image.height(); + + double aspect_ratio = (double)width / height; + + const std::map* type_sprite_map = &TeraTypeSpriteDatabase::instance().m_type_map; + + double best_score = 0.45; + PokemonTeraType best_type = PokemonTeraType::NONE; + for (const auto& item : *type_sprite_map){ + double expected_aspect_ratio = item.second.aspect_ratio(); + double ratio = aspect_ratio / expected_aspect_ratio; + + if (std::abs(ratio - 1) > 0.2){ + continue; + } + + double rmsd_alpha = item.second.matcher().diff(image); + + if (best_score > rmsd_alpha){ + best_score = rmsd_alpha; + best_type = item.first; + } + } + + return { best_score, best_type }; +} + +void find_tera_type_symbol_candidates( + std::multimap>& candidates, + const ImageViewPlanar32& original_screen, + const ImageViewRGB32& image, + PackedBinaryMatrix& matrix, + double max_area_ratio +){ + size_t max_area = (size_t)(image.width() * image.height() * max_area_ratio); + std::vector objects = find_objects_inplace( + matrix, + (size_t)(20. * original_screen.total_pixels() / (1920 * 1080)) + ); + + std::map objmap; + for (size_t c = 0; c < objects.size(); c++){ + if (objects[c].area > max_area){ + continue; + } + objmap[c] = objects[c]; + } + + bool changed; + do{ + changed = false; + for (auto iter0 = objmap.begin(); iter0 != objmap.end(); ++iter0){ + for (auto iter1 = objmap.begin(); iter1 != objmap.end();){ + if (iter0->first >= iter1->first){ + ++iter1; + continue; + } + const WaterfillObject& obj0 = iter0->second; + const WaterfillObject& obj1 = iter1->second; + size_t distance = distance_sqr( + ImagePixelBox(obj0.min_x, obj0.min_y, obj0.max_x, obj0.max_y), + ImagePixelBox(obj1.min_x, obj1.min_y, obj1.max_x, obj1.max_y) + ); + if (distance < 5 * 5){ + iter0->second.merge_assume_no_overlap(iter1->second); + iter1 = objmap.erase(iter1); + changed = true; + } else{ + ++iter1; + } + } + } + } + while (changed); + + for (const auto& item : objmap){ + ImageViewRGB32 img = extract_box_reference(image, item.second); + + std::pair result = match_tera_type_symbol(img); + if (result.second != PokemonTeraType::NONE){ + const WaterfillObject& obj = item.second; + candidates.emplace( + result.first, + std::pair( + result.second, + ImagePixelBox(obj.min_x, obj.min_y, obj.max_x, obj.max_y) + ) + ); + } + } +} + +} + +std::multimap> find_tera_type_symbols( + const ImageViewPlanar32& original_screen, + const ImageViewRGB32& image, + double max_area_ratio +){ + std::multimap> candidates; + + { + std::vector matrices = compress_rgb32_to_binary_range( + image, + { + {0xff808060, 0xffffffff}, + {0xffa0a060, 0xffffffff}, + {0xff606060, 0xffffffff}, + {0xff707070, 0xffffffff}, + {0xff808080, 0xffffffff}, + {0xff909090, 0xffffffff}, + {0xffa0a0a0, 0xffffffff}, + {0xffb0b0b0, 0xffffffff}, + {0xffc0c0c0, 0xffffffff}, + {0xffd0d0d0, 0xffffffff}, + {0xffe0e0e0, 0xffffffff}, + } + ); + for (PackedBinaryMatrix& matrix : matrices){ + find_tera_type_symbol_candidates(candidates, original_screen, image, matrix, max_area_ratio); + } + } + + std::multimap> filtered; + for (const auto& candidate : candidates){ + bool is_dupe = false; + for (const auto& item : filtered){ + if (distance_sqr(candidate.second.second, item.second.second) == 0){ + is_dupe = true; + break; + } + } + if (!is_dupe){ + filtered.emplace(candidate); + } + } + + return filtered; +} + +PokemonTeraType read_pokemon_tera_type( + const ImageViewRGB32& original_screen, + const ImageFloatBox& box +){ + ImageViewRGB32 image = extract_box_reference(original_screen, box); + + std::multimap> filtered = find_tera_type_symbols( + original_screen, + image, + 0.20 + ); + + if (filtered.empty()){ + return PokemonTeraType::NONE; + } + + return filtered.begin()->second.first; +} + +} +} diff --git a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TeraTypeReader.h b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TeraTypeReader.h new file mode 100644 index 0000000000..6c087ec17b --- /dev/null +++ b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TeraTypeReader.h @@ -0,0 +1,34 @@ +/* Tera Type Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonHome_TeraTypeReader_H +#define PokemonAutomation_PokemonHome_TeraTypeReader_H + +#include +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "Pokemon/Pokemon_Types.h" + +namespace PokemonAutomation { +namespace Pokemon { + +// Find all tera type symbols inside the image. +std::multimap> find_tera_type_symbols( + const ImageViewPlanar32& original_screen, + const ImageViewRGB32& image, + double max_area_ratio +); + +// Reads the tera type of a Pokemon. +PokemonTeraType read_pokemon_tera_type( + const ImageViewRGB32& original_screen, + const ImageFloatBox& box +); + +} +} + +#endif diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp index 3b14530b05..72e32d83f5 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp @@ -51,6 +51,7 @@ language #include "PokemonHome/Inference/PokemonHome_BoxGenderDetector.h" #include "PokemonHome/Inference/PokemonHome_BallReader.h" #include "PokemonHome/Inference/PokemonHome_GigantamaxDetector.h" +#include "PokemonHome/Inference/PokemonHome_TeraTypeReader.h" #include "PokemonHome_BoxSorter.h" namespace PokemonAutomation{ diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 61444756b2..c9725d6b86 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1553,6 +1553,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonHome/Inference/PokemonHome_ButtonDetector.h Source/PokemonHome/Inference/PokemonHome_GigantamaxDetector.cpp Source/PokemonHome/Inference/PokemonHome_GigantamaxDetector.h + Source/PokemonHome/Inference/PokemonHome_TeraTypeReader.cpp + Source/PokemonHome/Inference/PokemonHome_TeraTypeReader.h Source/PokemonHome/PokemonHome_Panels.cpp Source/PokemonHome/PokemonHome_Panels.h Source/PokemonHome/PokemonHome_Settings.cpp From eae2616a7c8ed078cee75d7e50a311c4289e5455 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Tue, 28 Apr 2026 08:16:08 -0500 Subject: [PATCH 7/7] Folder location --- .../Source/PokemonHome/Inference/PokemonHome_TeraTypeReader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TeraTypeReader.cpp b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TeraTypeReader.cpp index 69bbbf4483..b2011c908f 100644 --- a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TeraTypeReader.cpp +++ b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TeraTypeReader.cpp @@ -71,7 +71,7 @@ struct TeraTypeSpriteDatabase { if (item.first == PokemonTeraType::NONE){ continue; } - m_type_map.emplace(item.first, TypeSprite(item.second, "Pokemon/Types/Tera/" + item.second + ".png")); + m_type_map.emplace(item.first, TypeSprite(item.second, "PokemonHome/TeraTypes/" + item.second + ".png")); } } };