Skip to content

Commit 8b73d97

Browse files
authored
FRLG Summary stat reading initial implementation (#1130)
* FRLG Stat Reading via Template Match and PaddleOCR if enabled * Return farming panel * Level reader * Fix split crop overlapping * Better support for level reading * Disabled DEV mode FRLG panels * Reverted indentation * Fix compilation error * Changes based on PR feedback * Enhanced realiability and changes based on PR feedback * Indentation fix for digitreader, removed non ASCII characters
1 parent 8d25ad1 commit 8b73d97

8 files changed

Lines changed: 978 additions & 0 deletions

File tree

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/* FRLG Digit Reader
2+
*
3+
* From: https://github.com/PokemonAutomation/
4+
*
5+
*/
6+
7+
#include "PokemonFRLG_DigitReader.h"
8+
#include "Common/Cpp/Color.h" // needed for COLOR_RED, COLOR_ORANGE
9+
#include "Common/Cpp/Exceptions.h"
10+
#include "Common/Cpp/Logging/AbstractLogger.h"
11+
#include "CommonFramework/Globals.h"
12+
#include "CommonFramework/ImageTools/ImageBoxes.h"
13+
#include "CommonFramework/ImageTypes/ImageRGB32.h"
14+
#include "CommonFramework/ImageTypes/ImageViewRGB32.h"
15+
#include "CommonTools/ImageMatch/ExactImageMatcher.h"
16+
#include "CommonTools/Images/BinaryImage_FilterRgb32.h"
17+
#include "Kernels/Waterfill/Kernels_Waterfill_Session.h"
18+
#include <array>
19+
#include <map>
20+
#include <memory>
21+
#include <string>
22+
#include <vector>
23+
24+
#include <opencv2/imgproc.hpp>
25+
26+
#include <iostream>
27+
using std::cout;
28+
using std::endl;
29+
30+
namespace PokemonAutomation {
31+
namespace NintendoSwitch {
32+
namespace PokemonFRLG {
33+
34+
// ---------------------------------------------------------------------------
35+
// Template store: loads 10 digit matchers from a resource sub-directory.
36+
// Results are cached in a static map keyed by template type.
37+
// Supports both:
38+
// - StatBox (yellow stat boxes): PokemonFRLG/Digits/
39+
// - LevelBox (lilac level box): PokemonFRLG/LevelDigits/
40+
// ---------------------------------------------------------------------------
41+
42+
static std::string get_template_path(DigitTemplateType type) {
43+
switch (type) {
44+
case DigitTemplateType::StatBox:
45+
return "PokemonFRLG/Digits/";
46+
case DigitTemplateType::LevelBox:
47+
return "PokemonFRLG/LevelDigits/";
48+
default:
49+
return "PokemonFRLG/Digits/";
50+
}
51+
}
52+
53+
struct DigitTemplates {
54+
// matchers[d] is the matcher for digit d (0-9), or nullptr if missing.
55+
std::array<std::unique_ptr<ImageMatch::ExactImageMatcher>, 10> matchers;
56+
bool any_loaded = false;
57+
58+
explicit DigitTemplates(DigitTemplateType template_type) {
59+
std::string resource_subdir = get_template_path(template_type);
60+
for (int d = 0; d < 10; ++d) {
61+
std::string path =
62+
RESOURCE_PATH() + resource_subdir + std::to_string(d) + ".png";
63+
try {
64+
ImageRGB32 img(path);
65+
if (img.width() > 0) {
66+
matchers[d] =
67+
std::make_unique<ImageMatch::ExactImageMatcher>(std::move(img));
68+
any_loaded = true;
69+
}
70+
} catch (...) {
71+
// Template image missing - slot stays nullptr.
72+
}
73+
}
74+
if (!any_loaded) {
75+
throw FileException(nullptr, PA_CURRENT_FUNCTION,
76+
"Failed to load any digit templates", resource_subdir);
77+
}
78+
}
79+
80+
static const DigitTemplates& get(DigitTemplateType template_type) {
81+
static std::map<DigitTemplateType, DigitTemplates> cache;
82+
auto it = cache.find(template_type);
83+
if (it == cache.end()) {
84+
it = cache.emplace(template_type, DigitTemplates(template_type)).first;
85+
}
86+
return it->second;
87+
}
88+
};
89+
90+
// ---------------------------------------------------------------------------
91+
// Main function
92+
// ---------------------------------------------------------------------------
93+
int read_digits_waterfill_template(
94+
Logger& logger,
95+
const ImageViewRGB32& stat_region,
96+
double rmsd_threshold,
97+
DigitTemplateType template_type,
98+
const std::string& dump_prefix,
99+
uint8_t binarize_high
100+
) {
101+
using namespace Kernels::Waterfill;
102+
103+
if (!stat_region) {
104+
logger.log("DigitReader: empty stat region.", COLOR_RED);
105+
return -1;
106+
}
107+
108+
// ------------------------------------------------------------------
109+
// Step 1: Gaussian blur on the NATIVE resolution image.
110+
// The GBA pixel font has 1-pixel gaps between segments.
111+
// A 5x5 kernel applied twice bridges those gaps so that waterfill
112+
// sees each digit as a single connected component.
113+
// ------------------------------------------------------------------
114+
cv::Mat src = stat_region.to_opencv_Mat();
115+
cv::Mat blurred;
116+
src.copyTo(blurred);
117+
cv::GaussianBlur(blurred, blurred, cv::Size(5, 5), 1.5);
118+
cv::GaussianBlur(blurred, blurred, cv::Size(5, 5), 1.5);
119+
120+
ImageRGB32 blurred_img(blurred.cols, blurred.rows);
121+
blurred.copyTo(blurred_img.to_opencv_Mat());
122+
123+
// ------------------------------------------------------------------
124+
// Step 2: Binarise the blurred image.
125+
// Pixels where ALL channels <= binarize_high become 1 (foreground).
126+
// Default 0xBE (190) works for yellow stat boxes.
127+
// Use 0x7F (127) for the lilac level box to prevent the blurred
128+
// lilac background (B~208, drops to ~156 near shadows) from being
129+
// captured and merging digit blobs.
130+
// ------------------------------------------------------------------
131+
uint32_t bh = binarize_high;
132+
uint32_t binarize_color = 0xff000000u | (bh << 16) | (bh << 8) | bh;
133+
PackedBinaryMatrix matrix =
134+
compress_rgb32_to_binary_range(blurred_img, 0xff000000u, binarize_color);
135+
136+
// ------------------------------------------------------------------
137+
// Step 3: Waterfill - find connected dark blobs (individual digits).
138+
// Minimum area of 4 pixels to discard lone noise specks.
139+
// Sort blobs left-to-right by their left edge (min_x).
140+
// ------------------------------------------------------------------
141+
const size_t min_area = 4;
142+
std::map<size_t, WaterfillObject> blobs; // key = min_x, auto-sorted L->R
143+
{
144+
std::unique_ptr<WaterfillSession> session = make_WaterfillSession(matrix);
145+
auto iter = session->make_iterator(min_area);
146+
WaterfillObject obj;
147+
while (blobs.size() < 8 && iter->find_next(obj, false)) {
148+
// Require at least 3px wide AND 3px tall to discard noise fragments.
149+
if (obj.max_x - obj.min_x < 3 || obj.max_y - obj.min_y < 3)
150+
continue;
151+
// Use min_x as key so the map is automatically sorted left-to-right.
152+
// If two blobs share an identical min_x, bump the key slightly.
153+
size_t key = obj.min_x;
154+
while (blobs.count(key))
155+
++key;
156+
blobs.emplace(key, std::move(obj));
157+
}
158+
}
159+
160+
if (blobs.empty()) {
161+
logger.log("DigitReader: waterfill found no digit blobs.", COLOR_RED);
162+
return -1;
163+
}
164+
165+
// ------------------------------------------------------------------
166+
// Step 4: For each blob, crop the UNBLURRED original stat_region to
167+
// the blob's bounding box, then template-match against all 10 digit
168+
// templates using ExactImageMatcher::rmsd(). Pick the lowest RMSD.
169+
// ------------------------------------------------------------------
170+
const DigitTemplates& templates = DigitTemplates::get(template_type);
171+
std::string result_str;
172+
173+
for (const auto &kv : blobs) {
174+
const WaterfillObject &obj = kv.second;
175+
176+
size_t width = obj.max_x - obj.min_x;
177+
size_t height = obj.max_y - obj.min_y;
178+
179+
int expected_digits = 1;
180+
// GBA font digits are typically narrower than they are tall (aspect ~0.6).
181+
// If the blob's width is wider than expected for a single digit, it's a
182+
// merged blob.
183+
if (width > height * 1.5) {
184+
expected_digits = 3; // e.g. "100"
185+
} else if (width > height * 0.8) {
186+
expected_digits = 2; // e.g. "23"
187+
}
188+
189+
size_t split_w = width / expected_digits;
190+
191+
for (int i = 0; i < expected_digits; ++i) {
192+
size_t min_x = obj.min_x + i * split_w;
193+
size_t max_x = (i == expected_digits - 1) ? obj.max_x : obj.min_x + (i + 1) * split_w;
194+
195+
// Crop original (unblurred) region to the split bounding box.
196+
ImagePixelBox bbox(min_x, obj.min_y, max_x, obj.max_y);
197+
ImageViewRGB32 crop = extract_box_reference(stat_region, bbox);
198+
199+
if (dump_prefix == "levelDigit") {
200+
crop.save("DebugDumps/" + dump_prefix + "_x" + std::to_string(min_x) + "_split_raw.png");
201+
}
202+
203+
// Compute RMSD against each digit template; pick the minimum.
204+
// If no templates are loaded (extraction mode), skip matching entirely.
205+
double best_rmsd = 9999.0;
206+
int best_digit = -1;
207+
if (templates.any_loaded) {
208+
for (int d = 0; d < 10; ++d) {
209+
if (!templates.matchers[d])
210+
continue;
211+
double r = templates.matchers[d]->rmsd(crop);
212+
if (r < best_rmsd) {
213+
best_rmsd = r;
214+
best_digit = d;
215+
}
216+
}
217+
}
218+
219+
if (best_rmsd > rmsd_threshold) {
220+
// Always save the raw crop for user inspection / template extraction.
221+
crop.save("DebugDumps/" + dump_prefix + "_x" + std::to_string(min_x) +
222+
"_raw.png");
223+
logger.log(
224+
"DigitReader: blob at x=" + std::to_string(min_x) +
225+
" skipped (best RMSD=" + std::to_string(best_rmsd) +
226+
", threshold=" + std::to_string(rmsd_threshold) + ").",
227+
COLOR_ORANGE
228+
);
229+
continue;
230+
}
231+
232+
logger.log(
233+
"DigitReader: blob at x=" + std::to_string(min_x) +
234+
" -> digit " + std::to_string(best_digit) +
235+
" (RMSD=" + std::to_string(best_rmsd) + ")"
236+
);
237+
// Save crop with prefix so level and stat crops are distinguishable.
238+
crop.save("DebugDumps/" + dump_prefix + "_x" + std::to_string(min_x) +
239+
"_match" + std::to_string(best_digit) + ".png");
240+
result_str += static_cast<char>('0' + best_digit);
241+
}
242+
}
243+
244+
if (result_str.empty()) {
245+
return -1;
246+
}
247+
248+
int number = std::atoi(result_str.c_str());
249+
logger.log(
250+
"DigitReader: \"" + result_str + "\" -> " +
251+
std::to_string(number)
252+
);
253+
return number;
254+
}
255+
256+
} // namespace PokemonFRLG
257+
} // namespace NintendoSwitch
258+
} // namespace PokemonAutomation
259+
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* FRLG Digit Reader
2+
*
3+
* From: https://github.com/PokemonAutomation/
4+
*
5+
* Reads a string of decimal digits from a stat region using waterfill
6+
* segmentation on a blurred image to locate individual digit bounding boxes,
7+
* then template-matches each cropped digit against the pre-stored digit
8+
* templates (Resources/PokemonFRLG/Digits/0-9.png) on the unblurred original.
9+
*
10+
* This is the Tesseract/PaddleOCR-free fallback path for USE_PADDLE_OCR=false.
11+
*/
12+
13+
#ifndef PokemonAutomation_PokemonFRLG_DigitReader_H
14+
#include <cstdint>
15+
#include <string>
16+
17+
namespace PokemonAutomation {
18+
class Logger;
19+
class ImageViewRGB32;
20+
21+
namespace NintendoSwitch {
22+
namespace PokemonFRLG {
23+
24+
enum class DigitTemplateType {
25+
StatBox, // Yellow stat boxes (default): PokemonFRLG/Digits/
26+
LevelBox, // Lilac level box: PokemonFRLG/LevelDigits/
27+
};
28+
29+
// Read a string of decimal digits from `stat_region`.
30+
//
31+
// template_type Which template set to use (StatBox or LevelBox).
32+
// dump_prefix Prefix used when saving debug crop PNGs to DebugDumps/.
33+
//
34+
// Returns the parsed integer, or -1 on failure.
35+
int read_digits_waterfill_template(
36+
Logger& logger,
37+
const ImageViewRGB32& stat_region,
38+
double rmsd_threshold = 175.0,
39+
DigitTemplateType template_type = DigitTemplateType::StatBox,
40+
const std::string& dump_prefix = "digit",
41+
uint8_t binarize_high = 0xBE // 0xBE=190 for yellow stat boxes;
42+
// use 0x7F=127 for lilac level box
43+
);
44+
45+
// Read a string of decimal digits from `stat_region` by splitting the region into
46+
// a fixed number of equal-width segments, instead of using waterfill.
47+
// Useful when digits are tightly packed.
48+
//
49+
// num_splits The number of equal-width segments to split the region into.
50+
// template_type Which template set to use (StatBox or LevelBox).
51+
// dump_prefix Prefix used when saving debug crop PNGs to DebugDumps/.
52+
//
53+
// Returns the parsed integer, or -1 on failure.
54+
int read_digits_fixed_width_template(
55+
Logger& logger,
56+
const ImageViewRGB32& stat_region,
57+
int num_splits = 2,
58+
double rmsd_threshold = 175.0,
59+
DigitTemplateType template_type = DigitTemplateType::LevelBox,
60+
const std::string& dump_prefix = "digit_split"
61+
);
62+
63+
} // namespace PokemonFRLG
64+
} // namespace NintendoSwitch
65+
} // namespace PokemonAutomation
66+
67+
#endif
68+

0 commit comments

Comments
 (0)