diff --git a/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.cpp b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.cpp index fb576f0dc4..bc1fd93ef8 100644 --- a/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.cpp @@ -25,10 +25,11 @@ namespace PokemonFRLG{ BattlePokemonDetector::BattlePokemonDetector(Color color) // be warned: the name/level/hp box moves up and down a little bit - : m_left_box(0.577404, 0.504327, 0.001442, 0.121875) // off-white (255, 255, 236), can be interrupted by a status condition - , m_right_box(0.948558, 0.585096, 0.000481, 0.063462) // dark teal (74, 111, 102) + : m_left_box(0.577404, 0.504327, 0.001442, 0.121875) // off-white rgb(255, 255, 235), can be interrupted by a status condition + , m_right_box(0.948558, 0.585096, 0.000481, 0.063462) // dark teal rgb(72, 106, 98) , m_top_box(0.594712, 0.481971, 0.325962, 0.002163) // off-white, movement makes this unreliable , m_bottom_box(0.554808, 0.674519, 0.034615, 0.002163) // dark teal + , m_background_box(0.552564, 0.583653, 0.414103, 0.009615) // depends on in-game location. white or rgb(215, 169, 96) {} void BattlePokemonDetector::make_overlays(VideoOverlaySet& items) const{ const BoxOption& GAME_BOX = GameSettings::instance().GAME_BOX; @@ -44,10 +45,13 @@ bool BattlePokemonDetector::detect(const ImageViewRGB32& screen){ ImageViewRGB32 right_image = extract_box_reference(game_screen, m_right_box); ImageViewRGB32 top_image = extract_box_reference(game_screen, m_top_box); ImageViewRGB32 bottom_image = extract_box_reference(game_screen, m_bottom_box); - if (is_solid(left_image, { 0.3418, 0.3418, 0.3164 }) - && is_solid(right_image, { 0.2578, 0.3868, 0.3554 }, 0.075, 5) - && is_solid(top_image, { 0.3418, 0.3418, 0.3164 }) - && is_solid(bottom_image, { 0.2578, 0.3868, 0.3554 }, 0.075, 5) + ImageViewRGB32 background_image = extract_box_reference(game_screen, m_background_box); + + if (is_grey(left_image, 680, 770, 20) + && is_grey(right_image, 220, 320, 20) + && is_grey(top_image, 680, 770, 20) + && is_grey(bottom_image, 220, 320, 20) + && !(is_white(background_image) || is_solid(background_image, {0.4468, 0.3528, 0.2004}, 0.25, 20)) ){ return true; } @@ -55,10 +59,11 @@ bool BattlePokemonDetector::detect(const ImageViewRGB32& screen){ } BattleOpponentDetector::BattleOpponentDetector(Color color) - : m_left_box(0.067308, 0.140865, 0.001442, 0.090144) // off-white (255, 255, 236) + : m_left_box(0.067308, 0.140865, 0.001442, 0.090144) // off-white rgb(255, 255, 235) , m_right_box(0.422596, 0.132211, 0.000481, 0.054808) // off-white , m_top_box(0.085577, 0.119952, 0.329327, 0.000721) // off-white - , m_bottom_box(0.109615, 0.264182, 0.328365, 0.001442) // dark teal (74, 111, 102) + , m_bottom_box(0.109615, 0.264182, 0.328365, 0.001442) // dark teal rgb(72, 106, 98) + , m_background_box(0.050641, 0.126923, 0.388462, 0.063462) // depends on in-game location. white or rgb(187, 149, 65) {} void BattleOpponentDetector::make_overlays(VideoOverlaySet& items) const{ const BoxOption& GAME_BOX = GameSettings::instance().GAME_BOX; @@ -74,11 +79,13 @@ bool BattleOpponentDetector::detect(const ImageViewRGB32& screen){ ImageViewRGB32 right_image = extract_box_reference(game_screen, m_right_box); ImageViewRGB32 top_image = extract_box_reference(game_screen, m_top_box); ImageViewRGB32 bottom_image = extract_box_reference(game_screen, m_bottom_box); + ImageViewRGB32 background_image = extract_box_reference(game_screen, m_background_box); - if (is_solid(left_image, { 0.3418, 0.3418, 0.3164 }) - && is_solid(right_image, { 0.3418, 0.3418, 0.3164 }) - && is_solid(top_image, { 0.3418, 0.3418, 0.3164 }) - && is_solid(bottom_image, { 0.2578, 0.3868, 0.3554 }, 0.075, 5) + if (is_grey(left_image, 680, 770, 20) + && is_grey(right_image, 680, 770, 20) + && is_grey(top_image, 680, 770, 20) + && is_grey(bottom_image, 220, 320, 20) + && !(is_white(background_image) || is_solid(background_image, {0.4663, 0.3716, 0.1621}, 0.25, 20)) ){ return true; } diff --git a/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.h b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.h index a95a79c505..d561d1194e 100644 --- a/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.h +++ b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.h @@ -33,6 +33,7 @@ class BattlePokemonDetector : public StaticScreenDetector{ ImageFloatBox m_right_box; ImageFloatBox m_top_box; ImageFloatBox m_bottom_box; + ImageFloatBox m_background_box; }; // Watches for the player's Pokemon to disappear @@ -56,6 +57,7 @@ class BattleOpponentDetector : public StaticScreenDetector{ ImageFloatBox m_right_box; ImageFloatBox m_top_box; ImageFloatBox m_bottom_box; + ImageFloatBox m_background_box; }; // Watches for the opponent to disappear diff --git a/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_StatsReader.cpp b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_StatsReader.cpp index f0bf1ac0d5..7c518cfe9f 100644 --- a/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_StatsReader.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_StatsReader.cpp @@ -88,38 +88,58 @@ void StatsReader::read_page1( ImageViewRGB32 name_box = extract_box_reference(game_screen, jpn ? m_box_name_jpn : m_box_name); - // remove shadow - ImageRGB32 name_filtered = filter_rgb32_range( - name_box, 0xff000000, 0xffc7c7c7, Color(0xffc3c3c3), true - ); - // make text black - name_filtered = filter_rgb32_range( - name_filtered, 0xffc8c8c8, 0xffffffff, Color(0xff000000), true - ); - - ImageRGB32 name_ready = preprocess_for_ocr( - name_filtered, "name", 7, 2, true, - combine_rgb(0, 0, 0), jpn ? combine_rgb(160, 160, 160) : combine_rgb(120, 120, 120) - ); - - const std::vector name_text_color_ranges{ - {combine_rgb(0, 0, 0), combine_rgb(120, 120, 120)} - }; - if (subset.size() > 0){ - auto name_result = Pokemon::PokemonNameReader(subset).read_substring( - logger, language, name_ready, name_text_color_ranges - ); - if (!name_result.results.empty()){ - stats.name = name_result.results.begin()->second.token; + static const std::vector WHITE_THRESHOLDS = { 180, 200, 220, 230, 240 }; + OCR::StringMatchResult best_result; + bool initialized = false; + for (int thresh : WHITE_THRESHOLDS){ + ImageRGB32 name_filtered(name_box.width(), name_box.height()); + for (size_t r = 0; r < name_box.height(); r++){ + for (size_t c = 0; c < name_box.width(); c++){ + Color pixel(name_box.pixel(c, r)); + if (pixel.red() > thresh && pixel.green() > thresh && pixel.blue() > thresh){ + name_filtered.pixel(c, r) = (uint32_t)0xff000000; // Black + }else{ + name_filtered.pixel(c, r) = (uint32_t)0xffffffff; // White + } + } } - }else{ - auto name_result = Pokemon::PokemonNameReader::instance().read_substring( - logger, language, name_ready, name_text_color_ranges + ImageRGB32 name_ready = preprocess_for_ocr( + name_filtered, "name", 7, 2, true, + combine_rgb(0, 0, 0), jpn ? combine_rgb(160, 160, 160) : combine_rgb(140, 140, 140) ); - if (!name_result.results.empty()){ - stats.name = name_result.results.begin()->second.token; + + const std::vector name_text_color_ranges{ + {combine_rgb(0, 0, 0), combine_rgb(120, 120, 120)} + }; + + OCR::StringMatchResult result; + if (subset.size() > 0){ + auto name_result = Pokemon::PokemonNameReader(subset).read_substring( + logger, language, name_ready, name_text_color_ranges + ); + if (!name_result.results.empty()){ + result = name_result; + } + }else{ + auto name_result = Pokemon::PokemonNameReader::instance().read_substring( + logger, language, name_ready, name_text_color_ranges + ); + if (!name_result.results.empty()){ + result = name_result; + } } + if (!result.results.empty()){ + if (!initialized){ + best_result = result; + initialized = true; + }else if (result.results.begin()->first < best_result.results.begin()->first){ + best_result = result; + } + } + } + if (initialized && !best_result.results.empty()){ + stats.name = best_result.results.begin()->second.token; } // Detect gender by comparing red vs blue pixels @@ -148,42 +168,8 @@ void StatsReader::read_page1( stats.gender = SummaryGender::Genderless; } - - ImageViewRGB32 level_box = extract_box_reference(game_screen, jpn ? m_box_level_jpn : m_box_level); - ImageRGB32 level_upscaled = - level_box.scale_to(level_box.width() * 4, level_box.height() * 4); - if (save_debug_images){ - level_upscaled.save("DebugDumps/ocr_level_upscaled.png"); - } - - // The level has a colored (lilac) background. The text is white, with a - // gray/black shadow. To bridge the gaps and make a solid black character on a - // white background: We want to turn BOTH the bright white text AND the dark - // shadow into BLACK pixels, and turn the mid-tone lilac background into - // WHITE. We can do this by keeping pixels that are very bright (text) or very - // dark (shadow). - - ImageRGB32 level_ready(level_upscaled.width(), level_upscaled.height()); - for (size_t r = 0; r < level_upscaled.height(); r++){ - for (size_t c = 0; c < level_upscaled.width(); c++){ - Color pixel(level_upscaled.pixel(c, r)); - // If it's very bright (white text) OR very dark (shadow), it becomes - // black text. Otherwise (lilac background), it becomes white background. - if ((pixel.red() > 200 && pixel.green() > 200 && pixel.blue() > 200) || - (pixel.red() < 100 && pixel.green() < 100 && pixel.blue() < 100)){ - level_ready.pixel(c, r) = (uint32_t)0xff000000; // Black - }else{ - level_ready.pixel(c, r) = (uint32_t)0xffffffff; // White - } - } - } - - if (save_debug_images){ - level_ready.save("DebugDumps/ocr_level_ready.png"); - } - if (!GlobalSettings::instance().USE_PADDLE_OCR){ // The level uses white text with dark shadow on a lilac background. // The digit reader's binarizer captures dark pixels (<=190 on all channels) @@ -191,9 +177,20 @@ void StatsReader::read_page1( // shadow outline fragmented into many small disconnected blobs. // Preprocess: convert bright-white text pixels to black so the binarizer // merges text + shadow into one solid connected blob per digit. - ImageRGB32 preprocessed = filter_rgb32_range( - level_box, 0xffc8c8c8, 0xffffffff, Color(0xff000000), true - ); + ImageRGB32 preprocessed = level_box.scale_to(level_box.width(), level_box.height()); + for (size_t r = 0; r < level_box.height(); r++){ + for (size_t c = 0; c < level_box.width(); c++){ + Color pixel(level_box.pixel(c, r)); + // Try to detect lilac background first based on low green channel, + // replacing it with a darker lilac color (for matching the template) + // For other pixels, if it's bright it becomes black text. + if ((pixel.blue() > pixel.green() + 25) && (pixel.red() > pixel.green() + 15)){ + preprocessed.pixel(c, r) = (uint32_t)0xffd1b0f0; // from template + }else if (pixel.red() > 200 && pixel.green() > 200 && pixel.blue() > 200){ + preprocessed.pixel(c, r) = (uint32_t)0xff000000; // Black + } + } + } if (save_debug_images){ preprocessed.save("DebugDumps/ocr_level_preprocessed.png"); } @@ -215,6 +212,37 @@ void StatsReader::read_page1( logger, level_digit_view, 230.0, DigitTemplateType::LevelBox, "levelDigit", 0x7F); }else{ + // The level has a colored (lilac) background. The text is white, with a + // gray/black shadow. To bridge the gaps and make a solid black character on a + // white background: We want to turn BOTH the bright white text AND the dark + // shadow into BLACK pixels, and turn the mid-tone lilac background into + // WHITE. We can do this by keeping pixels that are very bright (text) or very + // dark (shadow). + ImageRGB32 level_upscaled = + level_box.scale_to(level_box.width() * 4, level_box.height() * 4); + if (save_debug_images){ + level_upscaled.save("DebugDumps/ocr_level_upscaled.png"); + } + ImageRGB32 level_ready(level_upscaled.width(), level_upscaled.height()); + for (size_t r = 0; r < level_upscaled.height(); r++){ + for (size_t c = 0; c < level_upscaled.width(); c++){ + Color pixel(level_upscaled.pixel(c, r)); + // Try to detect lilac background first based on low green channel. + // For other pixels, if it's very bright (white text) OR very dark (shadow), + // it becomes black text. Otherwise, it becomes white background. + if ((pixel.blue() > pixel.green() + 25) && (pixel.red() > pixel.green() + 15)){ + level_ready.pixel(c, r) = (uint32_t)0xffffffff; // White + }else if ((pixel.red() > 200 && pixel.green() > 200 && pixel.blue() > 200) || + (pixel.red() < 100 && pixel.green() < 100 && pixel.blue() < 100)){ + level_ready.pixel(c, r) = (uint32_t)0xff000000; // Black + }else{ + level_ready.pixel(c, r) = (uint32_t)0xffffffff; // White + } + } + } + if (save_debug_images){ + level_ready.save("DebugDumps/ocr_level_ready.png"); + } // Pass the binarized image to PaddleOCR stats.level = OCR::read_number(logger, level_ready, language); } diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp index 4ab65e0eff..62926992c0 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp @@ -463,7 +463,7 @@ void GiftRng::program(SingleSwitchProgramEnvironment& env, ProControllerContext& RNG_CALIBRATION.set_hits(search_hits); bool finished = update_history( env.console, uncertain_history, calibration_history, MAX_HISTORY_LENGTH, - calibrations, search_hits, 1 + calibrations, search_hits, 1, 2, MAX_RARE_CANDIES == 0 ); for (uint64_t i=0; i search_hits = get_wild_search_results(env.console, searcher, filters, SEED_VALUES, ADVANCES, advances_radius, gender_threshold, SUPER_ROD); RNG_CALIBRATION.set_hits(search_hits); - bool finished = update_history(env.console, uncertain_history, calibration_history, MAX_HISTORY_LENGTH, calibrations, search_hits, 1); + bool finished = update_history(env.console, uncertain_history, calibration_history, MAX_HISTORY_LENGTH, calibrations, search_hits, 1, 2, MAX_RARE_CANDIES == 0); finished = finished || all_indistinguishable(search_hits, searcher, gender_threshold, SUPER_ROD); for (uint64_t i=0; i