Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,51 @@ bool BattleOutOfPpDetector::detect(const ImageViewRGB32& screen){
return num_red_pixels > threshold;
}

BattleLevelUpDetector::BattleLevelUpDetector(Color color, BattleLevelUpDialog dialog_type)
: dialog_type(dialog_type)
, m_border_top_box(0.619231, 0.374038, 0.362179, 0.001923) // gray (120, 115, 140)
, m_border_right_box(0.982692, 0.376923, 0.001923, 0.597115)
, m_dialog_top_box(0.626282, 0.393492, 0.341026, 0.006175) // white
, m_dialog_right_box(0.967949, 0.401923, 0.003846, 0.550962)
, m_plus_box(0.862663, 0.402852, 0.034267, 0.553560)
{}
void BattleLevelUpDetector::make_overlays(VideoOverlaySet& items) const{
const BoxOption& GAME_BOX = GameSettings::instance().GAME_BOX;
items.add(COLOR_RED, GAME_BOX.inner_to_outer(m_border_top_box));
items.add(COLOR_RED, GAME_BOX.inner_to_outer(m_border_right_box));
items.add(COLOR_RED, GAME_BOX.inner_to_outer(m_dialog_top_box));
items.add(COLOR_RED, GAME_BOX.inner_to_outer(m_dialog_right_box));
}
bool BattleLevelUpDetector::detect(const ImageViewRGB32& screen){
ImageViewRGB32 game_screen = extract_box_reference(screen, GameSettings::instance().GAME_BOX);

//Border is teal
ImageViewRGB32 border_top_image = extract_box_reference(game_screen, m_border_top_box);
ImageViewRGB32 border_right_image = extract_box_reference(game_screen, m_border_right_box);

//Menu is white
ImageViewRGB32 dialog_top_image = extract_box_reference(game_screen, m_dialog_top_box);
ImageViewRGB32 dialog_right_image = extract_box_reference(game_screen, m_dialog_right_box);

//Plus box is not solid white on the first screen, but is white on the second one
ImageViewRGB32 plus_image = extract_box_reference(game_screen, m_plus_box);
bool good_plus_image = (
dialog_type == BattleLevelUpDialog::either
|| (dialog_type == BattleLevelUpDialog::plus && !is_white(plus_image))
|| (dialog_type == BattleLevelUpDialog::stats && is_white(plus_image))
);

if (is_solid(border_top_image, { 0.320, 0.307, 0.373 }, 0.25, 20)
&& is_solid(border_right_image, { 0.320, 0.307, 0.373 }, 0.25, 20)
&& is_white(dialog_top_image)
&& is_white(dialog_right_image)
&& good_plus_image
){
return true;
}
return false;
}



}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,35 @@ class BattleOutOfPpWatcher : public DetectorToFinder<BattleOutOfPpDetector>{
{}
};

enum class BattleLevelUpDialog{
plus,
stats,
either
};

class BattleLevelUpDetector : public StaticScreenDetector{
public:
BattleLevelUpDetector(Color color, BattleLevelUpDialog dialog_type);

virtual void make_overlays(VideoOverlaySet& items) const override;
virtual bool detect(const ImageViewRGB32& screen) override;

private:
BattleLevelUpDialog dialog_type;
ImageFloatBox m_border_top_box;
ImageFloatBox m_border_right_box;
ImageFloatBox m_dialog_top_box;
ImageFloatBox m_dialog_right_box;
ImageFloatBox m_plus_box;
};
class BattleLevelUpWatcher : public DetectorToFinder<BattleLevelUpDetector>{
public:
BattleLevelUpWatcher(Color color, BattleLevelUpDialog dialog_type)
: DetectorToFinder("BattleLevelUpWatcher", std::chrono::milliseconds(250), color, dialog_type)
{}
};




}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/* Battle Level Up Reader
*
* From: https://github.com/PokemonAutomation/
*
*/

#include "PokemonFRLG_BattleLevelUpReader.h"
#include "Common/Cpp/Color.h"
#include "Common/Cpp/Exceptions.h"
#include "CommonFramework/GlobalSettingsPanel.h"
#include "CommonFramework/ImageTypes/ImageViewRGB32.h"
#include "CommonFramework/Tools/GlobalThreadPools.h"
#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h"
#include "CommonTools/Images/ImageFilter.h"
#include "CommonTools/Images/ImageManip.h"
#include "CommonTools/OCR/OCR_NumberReader.h"
#include "CommonTools/OCR/OCR_Routines.h"
#include "Pokemon/Inference/Pokemon_NameReader.h"
#include "Pokemon/Inference/Pokemon_NatureReader.h"
#include "PokemonFRLG/PokemonFRLG_Settings.h"
#include "PokemonFRLG_DigitReader.h"
#include <opencv2/imgproc.hpp>

namespace PokemonAutomation {
namespace NintendoSwitch {
namespace PokemonFRLG {


BattleLevelUpReader::BattleLevelUpReader(Color color)
: m_color(color)
, m_box_hp(0.904069, 0.402852, 0.068535, 0.079387)
, m_box_attack(0.904069, 0.496052, 0.068535, 0.079387)
, m_box_defense(0.904069, 0.589252, 0.068535, 0.079387)
, m_box_sp_attack(0.904069, 0.682452, 0.068535, 0.079387)
, m_box_sp_defense(0.904069, 0.775652, 0.068535, 0.079387)
, m_box_speed(0.904069, 0.868852, 0.068535, 0.0793879)
{}

void BattleLevelUpReader::make_overlays(VideoOverlaySet &items) const {
const BoxOption &GAME_BOX = GameSettings::instance().GAME_BOX;
items.add(m_color, GAME_BOX.inner_to_outer(m_box_hp));
items.add(m_color, GAME_BOX.inner_to_outer(m_box_attack));
items.add(m_color, GAME_BOX.inner_to_outer(m_box_defense));
items.add(m_color, GAME_BOX.inner_to_outer(m_box_sp_attack));
items.add(m_color, GAME_BOX.inner_to_outer(m_box_sp_defense));
items.add(m_color, GAME_BOX.inner_to_outer(m_box_speed));
}

PokemonFRLG_LevelUpStats BattleLevelUpReader::read_stats(Logger &logger, const ImageViewRGB32& frame){
ImageViewRGB32 game_screen =
extract_box_reference(frame, GameSettings::instance().GAME_BOX);

auto read_stat = [&](const ImageFloatBox &box, const std::string &name){
ImageViewRGB32 stat_region = extract_box_reference(game_screen, box);

if (!GlobalSettings::instance().USE_PADDLE_OCR){
// Tesseract-free path: waterfill segmentation + template matching
// against the PokemonFRLG/Digits/0-9.png templates.
return read_digits_waterfill_template(logger, stat_region);
}

// PaddleOCR path (original): preprocess then per-digit waterfill OCR.
// Dark text [0..190] -> black. Threshold at 190 captures the
// blurred gap pixels between segments, making bridges thicker.
// Not higher than 190 to avoid capturing yellow bg edge noise.
ImageRGB32 ocr_ready = preprocess_for_ocr(
stat_region, name, 7, 2, true,
combine_rgb(0, 0, 0), combine_rgb(190, 190, 190)
);

// Waterfill isolates each digit -> per-char SINGLE_CHAR OCR.
return OCR::read_number_waterfill(
logger, ocr_ready, 0xff000000,
0xff808080
);
};

PokemonFRLG_LevelUpStats stats;
auto assign_stat = [](std::optional<unsigned>& field, int value){
if (value != -1){
field = static_cast<unsigned>(value);
}
};
assign_stat(stats.hp, read_stat(m_box_hp, "hp"));
assign_stat(stats.attack, read_stat(m_box_attack, "attack"));
assign_stat(stats.defense, read_stat(m_box_defense, "defense"));
assign_stat(stats.sp_attack, read_stat(m_box_sp_attack, "spatk"));
assign_stat(stats.sp_defense, read_stat(m_box_sp_defense, "spdef"));
assign_stat(stats.speed, read_stat(m_box_speed, "speed"));
return stats;
}


} // namespace PokemonFRLG
} // namespace NintendoSwitch
} // namespace PokemonAutomation

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* Battle Level Up Reader
*
* From: https://github.com/PokemonAutomation/
*
*/

#ifndef PokemonAutomation_PokemonFRLG_BattleLevelUpReader_H
#define PokemonAutomation_PokemonFRLG_BattleLevelUpReader_H

#include <optional>
#include <string>
#include "Common/Cpp/Color.h"
#include "CommonFramework/ImageTools/ImageBoxes.h"
#include "CommonFramework/Language.h"


namespace PokemonAutomation{

class Logger;
class ImageViewRGB32;
class VideoOverlaySet;

namespace NintendoSwitch{
namespace PokemonFRLG{

struct PokemonFRLG_LevelUpStats{
std::optional<unsigned> hp;
std::optional<unsigned> attack;
std::optional<unsigned> defense;
std::optional<unsigned> sp_attack;
std::optional<unsigned> sp_defense;
std::optional<unsigned> speed;
};

class BattleLevelUpReader {
public:
BattleLevelUpReader(Color color = COLOR_RED);

void make_overlays(VideoOverlaySet &items) const;

PokemonFRLG_LevelUpStats read_stats(Logger &logger, const ImageViewRGB32& frame);

private:
Color m_color;
ImageFloatBox m_box_hp;
ImageFloatBox m_box_attack;
ImageFloatBox m_box_defense;
ImageFloatBox m_box_sp_attack;
ImageFloatBox m_box_sp_defense;
ImageFloatBox m_box_speed;

};

} // namespace PokemonFRLG
} // namespace NintendoSwitch
} // namespace PokemonAutomation
#endif

Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@
#include "Common/Cpp/Logging/AbstractLogger.h"
#include "Kernels/Waterfill/Kernels_Waterfill_Session.h"
#include "CommonFramework/Globals.h"
#include "CommonFramework/GlobalSettingsPanel.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 "CommonTools/Images/ImageFilter.h"
#include "CommonTools/Images/ImageManip.h"

#include "PokemonFRLG_DigitReader.h"

//#include <iostream>
Expand All @@ -29,6 +33,108 @@ namespace PokemonAutomation{
namespace NintendoSwitch{
namespace PokemonFRLG{

// Debug counter for unique filenames
static int debug_counter = 0;

// Full OCR preprocessing pipeline for GBA pixel fonts.
//
// GBA fonts are seven-segment-like with 1-pixel gaps between segments.
// Pipeline: blur at native -> smooth upscale -> BW -> smooth BW -> re-BW -> pad
//
// The native blur connects gaps. Post-BW padding provides margins.
ImageRGB32 preprocess_for_ocr(
const ImageViewRGB32 &image,
const std::string &label,
int blur_kernel_size, int blur_passes,
bool in_range_black, uint32_t bw_min,
uint32_t bw_max
){
const bool save_debug_images = GlobalSettings::instance().SAVE_DEBUG_IMAGES;
int id = debug_counter++;
std::string prefix = "DebugDumps/ocr_" + label + "_" + std::to_string(id);

// Save raw input
if (save_debug_images){
image.save(prefix + "_0_raw.png");
}

cv::Mat src = image.to_opencv_Mat();

// Step 1: Gaussian blur at NATIVE resolution with 5x5 kernel.
// The 5x5 kernel reaches 2 pixels away (vs 1px for 3x3), bridging
// wider gaps in the seven-segment font. Two passes for heavy smoothing.
cv::Mat blurred_native;
src.copyTo(blurred_native);
if (blur_kernel_size > 0 && blur_passes > 0){
for (int i = 0; i < blur_passes; i++){
cv::GaussianBlur(
blurred_native, blurred_native,
cv::Size(blur_kernel_size, blur_kernel_size), 1.5
);
}
}

// Save blurred at native res
ImageRGB32 blurred_native_img(blurred_native.cols, blurred_native.rows);
blurred_native.copyTo(blurred_native_img.to_opencv_Mat());
if (save_debug_images){
blurred_native_img.save(prefix + "_1_blurred_native.png");
}

// Step 2: Smooth upscale 4x with bilinear interpolation.
int scale_factor = 4;
int new_w = static_cast<int>(image.width()) * scale_factor;
int new_h = static_cast<int>(image.height()) * scale_factor;
cv::Mat resized;
cv::resize(
blurred_native, resized, cv::Size(new_w, new_h), 0, 0,
cv::INTER_LINEAR
);

// Save upscaled
ImageRGB32 resized_img(resized.cols, resized.rows);
resized.copyTo(resized_img.to_opencv_Mat());
if (save_debug_images){
resized_img.save(prefix + "_2_upscaled.png");
}

// Step 3: BW threshold on the smooth upscaled image.
ImageRGB32 bw =
to_blackwhite_rgb32_range(resized_img, in_range_black, bw_min, bw_max);
if (save_debug_images){
bw.save(prefix + "_3_bw.png");
}

// Step 4: Post-BW smoothing -> re-threshold.
// The BW image has angular seven-segment shapes. GaussianBlur on the
// binary image creates gray anti-aliased edges. Re-thresholding at 128
// rounds the corners into natural smooth digit shapes that Tesseract
// recognizes much better. This is equivalent to morphological closing.
cv::Mat bw_mat = bw.to_opencv_Mat();
cv::Mat smoothed;
cv::GaussianBlur(bw_mat, smoothed, cv::Size(7, 7), 2.0);

// Re-threshold: convert smoothed back to ImageRGB32 and BW threshold.
// After blur on BW: text areas are dark gray (~0-64), bg areas are
// light gray (~192-255), edge zones are mid-gray (~64-192).
// Threshold at [0..128] captures text + expanded edges -> BLACK.
ImageRGB32 smoothed_img(smoothed.cols, smoothed.rows);
smoothed.copyTo(smoothed_img.to_opencv_Mat());
ImageRGB32 smooth_bw = to_blackwhite_rgb32_range(
smoothed_img, true, combine_rgb(0, 0, 0), combine_rgb(128, 128, 128));
if (save_debug_images){
smooth_bw.save(prefix + "_4_smooth_bw.png");
}

// Step 5: Pad with white border (Tesseract needs margins).
ImageRGB32 padded = pad_image(smooth_bw, smooth_bw.height() / 2, 0xffffffff);
if (save_debug_images){
padded.save(prefix + "_5_padded.png");
}

return padded;
}

// ---------------------------------------------------------------------------
// Template store: loads 10 digit matchers from a resource sub-directory.
// Results are cached in a static map keyed by template type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ enum class DigitTemplateType{
LevelBox, // Lilac level box: PokemonFRLG/LevelDigits/
};

// Full OCR preprocessing pipeline for GBA pixel fonts.
//
// GBA fonts are seven-segment-like with 1-pixel gaps between segments.
// Pipeline: blur at native -> smooth upscale -> BW -> smooth BW -> re-BW -> pad
//
// The native blur connects gaps. Post-BW padding provides margins.
ImageRGB32 preprocess_for_ocr(
const ImageViewRGB32 &image,
const std::string &label,
int blur_kernel_size, int blur_passes,
bool in_range_black, uint32_t bw_min,
uint32_t bw_max
);

// Read a string of decimal digits from `stat_region`.
//
// template_type Which template set to use (StatBox or LevelBox).
Expand Down
Loading