-
Notifications
You must be signed in to change notification settings - Fork 94
FRLG Summary stat reading initial implementation #1130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
miniumknight
wants to merge
12
commits into
PokemonAutomation:main
Choose a base branch
from
miniumknight:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+803
−0
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
ba70d94
FRLG Stat Reading via Template Match and PaddleOCR if enabled
miniumknight 05dd438
Merge resolution
miniumknight 4ea9409
Return farming panel
miniumknight 1357e09
Level reader
miniumknight 0f417f0
Fix split crop overlapping
miniumknight 7c61669
Better support for level reading
miniumknight 46b2110
Merge branch 'PokemonAutomation:main' into main
miniumknight 9f9684a
Merge branch 'main' of https://github.com/miniumknight/Arduino-Source
miniumknight e031d89
Disabled DEV mode FRLG panels
miniumknight d88bc89
Reverted indentation
miniumknight aadb0d0
Fix compilation error
miniumknight 19163af
Changes based on PR feedback
miniumknight File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
251 changes: 251 additions & 0 deletions
251
SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_DigitReader.cpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,251 @@ | ||
| /* FRLG Digit Reader | ||
| * | ||
| * From: https://github.com/PokemonAutomation/ | ||
| * | ||
| */ | ||
|
|
||
| #include "PokemonFRLG_DigitReader.h" | ||
| #include "Common/Cpp/Color.h" // needed for COLOR_RED, COLOR_ORANGE | ||
| #include "Common/Cpp/Exceptions.h" | ||
| #include "Common/Cpp/Logging/AbstractLogger.h" | ||
| #include "CommonFramework/Globals.h" | ||
| #include "CommonFramework/ImageTools/ImageBoxes.h" | ||
| #include "CommonFramework/ImageTypes/ImageRGB32.h" | ||
| #include "CommonFramework/ImageTypes/ImageViewRGB32.h" | ||
| #include "CommonTools/ImageMatch/ExactImageMatcher.h" | ||
| #include "CommonTools/Images/BinaryImage_FilterRgb32.h" | ||
| #include "Kernels/Waterfill/Kernels_Waterfill_Session.h" | ||
| #include <array> | ||
| #include <map> | ||
| #include <memory> | ||
| #include <string> | ||
| #include <vector> | ||
|
|
||
| #include <opencv2/imgproc.hpp> | ||
|
|
||
| #include <iostream> | ||
| using std::cout; | ||
| using std::endl; | ||
|
|
||
| namespace PokemonAutomation { | ||
| namespace NintendoSwitch { | ||
| namespace PokemonFRLG { | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Template store: loads 10 digit matchers from a resource sub-directory. | ||
| // Results are cached in a static map keyed by template type. | ||
| // Supports both: | ||
| // - StatBox (yellow stat boxes): PokemonFRLG/Digits/ | ||
| // - LevelBox (lilac level box): PokemonFRLG/LevelDigits/ | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| static std::string get_template_path(DigitTemplateType type) { | ||
| switch (type) { | ||
| case DigitTemplateType::StatBox: | ||
| return "PokemonFRLG/Digits/"; | ||
| case DigitTemplateType::LevelBox: | ||
| return "PokemonFRLG/LevelDigits/"; | ||
| default: | ||
| return "PokemonFRLG/Digits/"; | ||
| } | ||
| } | ||
|
|
||
| struct DigitTemplates { | ||
| // matchers[d] is the matcher for digit d (0-9), or nullptr if missing. | ||
| std::array<std::unique_ptr<ImageMatch::ExactImageMatcher>, 10> matchers; | ||
| bool any_loaded = false; | ||
|
|
||
| explicit DigitTemplates(DigitTemplateType template_type) { | ||
| std::string resource_subdir = get_template_path(template_type); | ||
| for (int d = 0; d < 10; ++d) { | ||
| std::string path = | ||
| RESOURCE_PATH() + resource_subdir + std::to_string(d) + ".png"; | ||
| try { | ||
| ImageRGB32 img(path); | ||
| if (img.width() > 0) { | ||
| matchers[d] = | ||
| std::make_unique<ImageMatch::ExactImageMatcher>(std::move(img)); | ||
| any_loaded = true; | ||
| } | ||
| } catch (...) { | ||
| // Template image missing - slot stays nullptr. | ||
| } | ||
| } | ||
| if (!any_loaded) { | ||
| throw FileException(nullptr, PA_CURRENT_FUNCTION, | ||
| "Failed to load any digit templates", resource_subdir); | ||
| } | ||
| } | ||
|
|
||
| static const DigitTemplates& get(DigitTemplateType template_type) { | ||
| static std::map<DigitTemplateType, DigitTemplates> cache; | ||
| auto it = cache.find(template_type); | ||
| if (it == cache.end()) { | ||
| it = cache.emplace(template_type, DigitTemplates(template_type)).first; | ||
| } | ||
| return it->second; | ||
| } | ||
| }; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Main function | ||
| // --------------------------------------------------------------------------- | ||
| int read_digits_waterfill_template(Logger& logger, | ||
| const ImageViewRGB32& stat_region, | ||
| double rmsd_threshold, | ||
| DigitTemplateType template_type, | ||
| const std::string& dump_prefix, | ||
| uint8_t binarize_high) { | ||
| using namespace Kernels::Waterfill; | ||
|
|
||
| if (!stat_region) { | ||
| logger.log("DigitReader: empty stat region.", COLOR_RED); | ||
| return -1; | ||
| } | ||
|
|
||
| // ------------------------------------------------------------------ | ||
| // Step 1: Gaussian blur on the NATIVE resolution image. | ||
| // The GBA pixel font has 1-pixel gaps between segments. | ||
| // A 5x5 kernel applied twice bridges those gaps so that waterfill | ||
| // sees each digit as a single connected component. | ||
| // ------------------------------------------------------------------ | ||
| cv::Mat src = stat_region.to_opencv_Mat(); | ||
| cv::Mat blurred; | ||
| src.copyTo(blurred); | ||
| cv::GaussianBlur(blurred, blurred, cv::Size(5, 5), 1.5); | ||
| cv::GaussianBlur(blurred, blurred, cv::Size(5, 5), 1.5); | ||
|
|
||
| ImageRGB32 blurred_img(blurred.cols, blurred.rows); | ||
| blurred.copyTo(blurred_img.to_opencv_Mat()); | ||
|
|
||
| // ------------------------------------------------------------------ | ||
| // Step 2: Binarise the blurred image. | ||
| // Pixels where ALL channels <= binarize_high become 1 (foreground). | ||
| // Default 0xBE (190) works for yellow stat boxes. | ||
| // Use 0x7F (127) for the lilac level box to prevent the blurred | ||
| // lilac background (B≈208, drops to ~156 near shadows) from being | ||
| // captured and merging digit blobs. | ||
| // ------------------------------------------------------------------ | ||
| uint32_t bh = binarize_high; | ||
| uint32_t binarize_color = 0xff000000u | (bh << 16) | (bh << 8) | bh; | ||
| PackedBinaryMatrix matrix = | ||
| compress_rgb32_to_binary_range(blurred_img, 0xff000000u, binarize_color); | ||
|
|
||
| // ------------------------------------------------------------------ | ||
| // Step 3: Waterfill - find connected dark blobs (individual digits). | ||
| // Minimum area of 4 pixels to discard lone noise specks. | ||
| // Sort blobs left-to-right by their left edge (min_x). | ||
| // ------------------------------------------------------------------ | ||
| const size_t min_area = 4; | ||
| std::map<size_t, WaterfillObject> blobs; // key = min_x, auto-sorted L->R | ||
| { | ||
| std::unique_ptr<WaterfillSession> session = make_WaterfillSession(matrix); | ||
| auto iter = session->make_iterator(min_area); | ||
| WaterfillObject obj; | ||
| while (blobs.size() < 8 && iter->find_next(obj, false)) { | ||
| // Require at least 3px wide AND 3px tall to discard noise fragments. | ||
| if (obj.max_x - obj.min_x < 3 || obj.max_y - obj.min_y < 3) | ||
| continue; | ||
| // Use min_x as key so the map is automatically sorted left-to-right. | ||
| // If two blobs share an identical min_x, bump the key slightly. | ||
| size_t key = obj.min_x; | ||
| while (blobs.count(key)) | ||
| ++key; | ||
| blobs.emplace(key, std::move(obj)); | ||
| } | ||
| } | ||
|
|
||
| if (blobs.empty()) { | ||
| logger.log("DigitReader: waterfill found no digit blobs.", COLOR_RED); | ||
| return -1; | ||
| } | ||
|
|
||
| // ------------------------------------------------------------------ | ||
| // Step 4: For each blob, crop the UNBLURRED original stat_region to | ||
| // the blob's bounding box, then template-match against all 10 digit | ||
| // templates using ExactImageMatcher::rmsd(). Pick the lowest RMSD. | ||
| // ------------------------------------------------------------------ | ||
| const DigitTemplates& templates = DigitTemplates::get(template_type); | ||
| std::string result_str; | ||
|
|
||
| for (const auto &kv : blobs) { | ||
| const WaterfillObject &obj = kv.second; | ||
|
|
||
| size_t width = obj.max_x - obj.min_x; | ||
| size_t height = obj.max_y - obj.min_y; | ||
|
|
||
| int expected_digits = 1; | ||
| // GBA font digits are typically narrower than they are tall (aspect ~0.6). | ||
| // If the blob's width is wider than expected for a single digit, it's a | ||
| // merged blob. | ||
| if (width > height * 1.5) { | ||
| expected_digits = 3; // e.g. "100" | ||
| } else if (width > height * 0.8) { | ||
| expected_digits = 2; // e.g. "23" | ||
| } | ||
|
|
||
| size_t split_w = width / expected_digits; | ||
|
|
||
| for (int i = 0; i < expected_digits; ++i) { | ||
| size_t min_x = obj.min_x + i * split_w; | ||
| size_t max_x = (i == expected_digits - 1) ? obj.max_x : obj.min_x + (i + 1) * split_w; | ||
|
|
||
| // Crop original (unblurred) region to the split bounding box. | ||
| ImagePixelBox bbox(min_x, obj.min_y, max_x, obj.max_y); | ||
| ImageViewRGB32 crop = extract_box_reference(stat_region, bbox); | ||
|
|
||
| if (dump_prefix == "levelDigit") { | ||
| crop.save("DebugDumps/" + dump_prefix + "_x" + std::to_string(min_x) + "_split_raw.png"); | ||
| } | ||
|
|
||
| // Compute RMSD against each digit template; pick the minimum. | ||
| // If no templates are loaded (extraction mode), skip matching entirely. | ||
| double best_rmsd = 9999.0; | ||
| int best_digit = -1; | ||
| if (templates.any_loaded) { | ||
| for (int d = 0; d < 10; ++d) { | ||
| if (!templates.matchers[d]) | ||
| continue; | ||
| double r = templates.matchers[d]->rmsd(crop); | ||
| if (r < best_rmsd) { | ||
| best_rmsd = r; | ||
| best_digit = d; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (best_rmsd > rmsd_threshold) { | ||
| // Always save the raw crop for user inspection / template extraction. | ||
| crop.save("DebugDumps/" + dump_prefix + "_x" + std::to_string(min_x) + | ||
| "_raw.png"); | ||
| logger.log("DigitReader: blob at x=" + std::to_string(min_x) + | ||
| " skipped (best RMSD=" + std::to_string(best_rmsd) + | ||
| ", threshold=" + std::to_string(rmsd_threshold) + ").", | ||
| COLOR_ORANGE); | ||
| continue; | ||
| } | ||
|
|
||
| logger.log("DigitReader: blob at x=" + std::to_string(min_x) + | ||
| " -> digit " + std::to_string(best_digit) + | ||
| " (RMSD=" + std::to_string(best_rmsd) + ")"); | ||
| // Save crop with prefix so level and stat crops are distinguishable. | ||
| crop.save("DebugDumps/" + dump_prefix + "_x" + std::to_string(min_x) + | ||
| "_match" + std::to_string(best_digit) + ".png"); | ||
| result_str += static_cast<char>('0' + best_digit); | ||
| } | ||
| } | ||
|
|
||
| if (result_str.empty()) { | ||
| return -1; | ||
| } | ||
|
|
||
| int number = std::atoi(result_str.c_str()); | ||
| logger.log("DigitReader: \"" + result_str + "\" -> " + | ||
| std::to_string(number)); | ||
| return number; | ||
| } | ||
|
|
||
| } // namespace PokemonFRLG | ||
| } // namespace NintendoSwitch | ||
| } // namespace PokemonAutomation | ||
|
|
68 changes: 68 additions & 0 deletions
68
SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_DigitReader.h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| /* FRLG Digit Reader | ||
| * | ||
| * From: https://github.com/PokemonAutomation/ | ||
| * | ||
| * Reads a string of decimal digits from a stat region using waterfill | ||
| * segmentation on a blurred image to locate individual digit bounding boxes, | ||
| * then template-matches each cropped digit against the pre-stored digit | ||
| * templates (Resources/PokemonFRLG/Digits/0-9.png) on the unblurred original. | ||
| * | ||
| * This is the Tesseract/PaddleOCR-free fallback path for USE_PADDLE_OCR=false. | ||
| */ | ||
|
|
||
| #ifndef PokemonAutomation_PokemonFRLG_DigitReader_H | ||
| #include <cstdint> | ||
| #include <string> | ||
|
|
||
| namespace PokemonAutomation { | ||
| class Logger; | ||
| class ImageViewRGB32; | ||
|
|
||
| namespace NintendoSwitch { | ||
| namespace PokemonFRLG { | ||
|
|
||
| enum class DigitTemplateType { | ||
| StatBox, // Yellow stat boxes (default): PokemonFRLG/Digits/ | ||
| LevelBox, // Lilac level box: PokemonFRLG/LevelDigits/ | ||
| }; | ||
|
|
||
| // Read a string of decimal digits from `stat_region`. | ||
| // | ||
| // template_type Which template set to use (StatBox or LevelBox). | ||
| // dump_prefix Prefix used when saving debug crop PNGs to DebugDumps/. | ||
| // | ||
| // Returns the parsed integer, or -1 on failure. | ||
| int read_digits_waterfill_template( | ||
| Logger& logger, | ||
| const ImageViewRGB32& stat_region, | ||
| double rmsd_threshold = 175.0, | ||
| DigitTemplateType template_type = DigitTemplateType::StatBox, | ||
| const std::string& dump_prefix = "digit", | ||
| uint8_t binarize_high = 0xBE // 0xBE=190 for yellow stat boxes; | ||
| // use 0x7F=127 for lilac level box | ||
| ); | ||
|
|
||
| // Read a string of decimal digits from `stat_region` by splitting the region into | ||
| // a fixed number of equal-width segments, instead of using waterfill. | ||
| // Useful when digits are tightly packed or overlapping and waterfill merges them. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When are the digit overlapping? |
||
| // | ||
| // num_splits The number of equal-width segments to split the region into. | ||
| // template_type Which template set to use (StatBox or LevelBox). | ||
| // dump_prefix Prefix used when saving debug crop PNGs to DebugDumps/. | ||
| // | ||
| // Returns the parsed integer, or -1 on failure. | ||
| int read_digits_fixed_width_template( | ||
| Logger& logger, | ||
| const ImageViewRGB32& stat_region, | ||
| int num_splits = 2, | ||
| double rmsd_threshold = 175.0, | ||
| DigitTemplateType template_type = DigitTemplateType::LevelBox, | ||
| const std::string& dump_prefix = "digit_split" | ||
| ); | ||
|
|
||
| } // namespace PokemonFRLG | ||
| } // namespace NintendoSwitch | ||
| } // namespace PokemonAutomation | ||
|
|
||
| #endif | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indent is 4.