Skip to content

Commit d06a124

Browse files
author
Gin
committed
Add box cursor detection whild holding a pokemon
1 parent bd734b6 commit d06a124

File tree

2 files changed

+150
-11
lines changed

2 files changed

+150
-11
lines changed

SerialPrograms/Source/PokemonLZA/Inference/Boxes/PokemonLZA_BoxDetection.cpp

Lines changed: 139 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,21 @@
66

77
#include "Common/Cpp/Exceptions.h"
88
#include "CommonFramework/Exceptions/FatalProgramException.h"
9+
#include "CommonFramework/Globals.h"
10+
#include "CommonFramework/GlobalSettingsPanel.h"
911
#include "CommonFramework/ImageTools/ImageStats.h"
12+
#include "CommonFramework/ImageTypes/ImageRGB32.h"
13+
#include "CommonFramework/ImageTypes/ImageHSV32.h"
1014
#include "CommonFramework/VideoPipeline/VideoOverlay.h"
1115
#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h"
1216
#include "CommonFramework/Tools/ErrorDumper.h"
1317
#include "CommonFramework/Tools/VideoStream.h"
1418
#include "CommonFramework/VideoPipeline/VideoFeed.h"
1519
#include "CommonTools/Images/SolidColorTest.h"
1620
#include "CommonTools/ImageMatch/SubObjectTemplateMatcher.h"
21+
#include "CommonTools/ImageMatch/WaterfillTemplateMatcher.h"
1722
#include "CommonTools/Images/BinaryImage_FilterRgb32.h"
23+
#include "CommonTools/Images/WaterfillUtilities.h"
1824
#include "Kernels/Waterfill/Kernels_Waterfill.h"
1925
#include "Kernels/Waterfill/Kernels_Waterfill_Session.h"
2026
#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h"
@@ -36,6 +42,7 @@ namespace{
3642
bool debug_switch = false;
3743
}
3844

45+
3946
// match the downward arrow (green border, white interior) in the box system view
4047
class BoxCellSelectionArrowMatcher : public ImageMatch::SubObjectTemplateMatcher{
4148
public:
@@ -58,19 +65,101 @@ class BoxCellSelectionArrowMatcher : public ImageMatch::SubObjectTemplateMatcher
5865
m_path
5966
);
6067
}
61-
68+
6269
set_subobject(objects[0]);
6370

71+
// Analyze template HSV values to determine green hue range
72+
// analyze_template_hsv();
73+
6474
// extract_box_reference(m_matcher.image_template(), objects[0]).save("template.png");
6575
}
6676

77+
// Optional custom image validation hook called before rmsd checks.
78+
// Check if the input image contains green pixels with hue close to 51
79+
virtual bool check_image(const ImageViewRGB32& input_image) const override{
80+
// Convert RGB to HSV
81+
ImageHSV32 input_hsv(input_image);
82+
ImageViewHSV32 hsv_view(input_hsv);
83+
84+
// Target hue for green arrow border
85+
const uint32_t target_hue = 50;
86+
const uint32_t hue_tolerance = 10;
87+
const uint32_t min_hue = target_hue - hue_tolerance;
88+
const uint32_t max_hue = target_hue + hue_tolerance;
89+
90+
// Minimum saturation and value to avoid detecting grayish/dark pixels as green
91+
const uint32_t min_saturation = 30; // At least 30/255 saturation
92+
const uint32_t min_value = 30; // At least 30/255 brightness
93+
94+
// Count pixels with green hue
95+
size_t green_pixel_count = 0;
96+
const size_t total_pixels = input_image.width() * input_image.height();
97+
98+
for (size_t y = 0; y < hsv_view.height(); y++){
99+
for (size_t x = 0; x < hsv_view.width(); x++){
100+
uint32_t hsv_pixel = hsv_view.pixel(x, y);
101+
uint32_t hue = (hsv_pixel >> 16) & 0xFF;
102+
uint32_t sat = (hsv_pixel >> 8) & 0xFF;
103+
uint32_t val = hsv_pixel & 0xFF;
104+
105+
// Check if pixel is green with target hue
106+
if (hue >= min_hue && hue <= max_hue &&
107+
sat >= min_saturation &&
108+
val >= min_value){
109+
green_pixel_count++;
110+
}
111+
}
112+
}
113+
114+
// Require at least 33% of pixels to be green
115+
const size_t min_required_green_pixels = total_pixels / 3;
116+
const bool has_green = green_pixel_count >= min_required_green_pixels;
117+
118+
if (debug_switch){
119+
static int counter = 0;
120+
cout << "check_image() input image: check_image_input_" << counter << ".png" << endl;
121+
input_image.save("check_image_input_" + std::to_string(counter++) + ".png");
122+
cout << "check_image() HSV validation:" << endl;
123+
cout << " Green pixels found: " << green_pixel_count << " / " << total_pixels
124+
<< " (" << (100.0 * green_pixel_count / total_pixels) << "%)" << endl;
125+
cout << " Required minimum: " << min_required_green_pixels
126+
<< " (" << (100.0 * min_required_green_pixels / total_pixels) << "%)" << endl;
127+
cout << " Hue range: [" << min_hue << ", " << max_hue << "]" << endl;
128+
cout << " Result: " << (has_green ? "PASS" : "FAIL") << endl;
129+
}
130+
131+
return has_green;
132+
};
133+
67134
static const BoxCellSelectionArrowMatcher& matcher(){
68135
static BoxCellSelectionArrowMatcher matcher;
69136
return matcher;
70137
}
71138
};
72139

73140

141+
class BoxCellSelectionArrowGreenPartMatcher : public ImageMatch::WaterfillTemplateMatcher{
142+
public:
143+
BoxCellSelectionArrowGreenPartMatcher();
144+
145+
static const BoxCellSelectionArrowGreenPartMatcher& instance();
146+
};
147+
148+
BoxCellSelectionArrowGreenPartMatcher::BoxCellSelectionArrowGreenPartMatcher()
149+
: WaterfillTemplateMatcher(
150+
"PokemonLZA/SelectionArrowDown.png",
151+
Color(150, 200, 20),
152+
Color(200, 250, 50),
153+
100
154+
)
155+
{}
156+
157+
const BoxCellSelectionArrowGreenPartMatcher& BoxCellSelectionArrowGreenPartMatcher::instance(){
158+
static BoxCellSelectionArrowGreenPartMatcher matcher;
159+
return matcher;
160+
}
161+
162+
74163
BoxDetector::BoxDetector(Color color, VideoOverlay* overlay)
75164
: m_color(color)
76165
, m_plus_button(color, ButtonType::ButtonPlus, {0.581, 0.940, 0.310, 0.046}, overlay)
@@ -80,6 +169,7 @@ BoxDetector::BoxDetector(Color color, VideoOverlay* overlay)
80169
for(size_t col = 0; col < 6; col++){
81170
double x = 0.058 + col * (0.386 - 0.059)/5.0;
82171
m_arrow_boxes.emplace_back(x, y, 0.018, 0.026);
172+
m_lifted_arrow_boxes.emplace_back(x+0.011, y-0.010, 0.023, 0.032);
83173
}
84174
}
85175
}
@@ -91,11 +181,46 @@ void BoxDetector::make_overlays(VideoOverlaySet& items) const{
91181
}
92182
}
93183

184+
94185
// detect arrow's white interior first
95186
// then use subobject template matcher BoxCellSelectionArrowMatcher to detect the whole arrow
96-
bool BoxDetector::detect_at_cell(const ImageViewRGB32& image_crop){
97-
// the arrow's white interior has color between rgb [220, 220, 220] to [255, 255, 255]
98-
// the arrow's green border has color between rgb [170, 230, 50] to [190, 255, 80]
187+
bool BoxDetector::detect_at_cell(const Resolution& screen_resolution, const ImageViewRGB32& image_crop){
188+
if (m_holding_pokemon){
189+
// If the box cursor is holding a pokemon, it will be a green downard arrow with white interior.
190+
// This green arrow may appear on top of the bottom part of a pokemon on the upper row of the box.
191+
// If the pokemon above has white bottom part, like a regular-color Spewpa, the arrrow white interior
192+
// blends in with the Spewpa white color and make it impossible to find the arrow interior using
193+
// waterfill. So we do a waterfill for the green part of the arrow against non-green background here.
194+
const std::vector<std::pair<uint32_t, uint32_t>> FILTERS = {
195+
{combine_rgb(170, 230, 50), combine_rgb(200, 255, 100)},
196+
{combine_rgb(140, 180, 0), combine_rgb(200, 255, 100)},
197+
};
198+
const double screen_rel_size = (screen_resolution.height / 1080.0);
199+
const size_t min_area = static_cast<size_t>(250 * screen_rel_size * screen_rel_size);
200+
const double rmsd_threshold = 70.0;
201+
const bool detected = match_template_by_waterfill(
202+
screen_resolution,
203+
image_crop,
204+
BoxCellSelectionArrowGreenPartMatcher::instance(),
205+
FILTERS,
206+
{min_area, SIZE_MAX},
207+
rmsd_threshold,
208+
[&](Kernels::Waterfill::WaterfillObject& object) -> bool {
209+
return true;
210+
}
211+
);
212+
if (detected){
213+
return true;
214+
}
215+
}
216+
217+
// The box curosr either is not holding a pokemon, or it is holding a pokemon but the background of the cursor
218+
// happens to be green (bottom part of a green pokemon) failing the arrow green part waterfill detection.
219+
// In both cases, we try to use waterfill to find the white interior of the arrow and then match the full arrow
220+
// tempalte image around the found white interior:
221+
222+
// The arrow's white interior has color between rgb [220, 220, 220] to [255, 255, 255]
223+
// The arrow's green border has color between rgb [170, 230, 50] to [190, 255, 80]
99224
std::vector<std::pair<uint32_t, uint32_t>> filters = {
100225
{0xff808080, 0xffffffff},
101226
{0xff909090, 0xffffffff},
@@ -147,9 +272,9 @@ bool BoxDetector::detect_at_cell(const ImageViewRGB32& image_crop){
147272
}
148273

149274
if (debug_switch){
150-
cout << "Find object: area " << object.area << ", save to found_object" << saved_object_id << ".png" << endl;
275+
cout << "Find object: area " << object.area << ", save to found_subobject" << saved_object_id << ".png" << endl;
151276
ImagePixelBox box(object);
152-
extract_box_reference(image_crop, box).save("found_object" + std::to_string(saved_object_id++) + ".png");
277+
extract_box_reference(image_crop, box).save("found_subobject" + std::to_string(saved_object_id++) + ".png");
153278
}
154279
if (object.area > max_area){
155280
continue;
@@ -186,6 +311,7 @@ bool BoxDetector::detect_at_cell(const ImageViewRGB32& image_crop){
186311
return detected;
187312
}
188313

314+
189315
bool BoxDetector::detect(const ImageViewRGB32& screen){
190316
if (!m_plus_button.detect(screen)){
191317
return false;
@@ -201,16 +327,19 @@ bool BoxDetector::detect(const ImageViewRGB32& screen){
201327
}
202328
cout << "row = " << (int)row << ", col = " << (int)col << endl;
203329
#endif
204-
ImageViewRGB32 image_crop = extract_box_reference(screen, m_arrow_boxes[cell_idx]);
330+
const auto& box = (m_holding_pokemon ? m_lifted_arrow_boxes[cell_idx] : m_arrow_boxes[cell_idx]);
331+
ImageViewRGB32 image_crop = extract_box_reference(screen, box);
205332
// image_crop.save("cell_" + std::to_string(row) + "_" + std::to_string(col) + ".png");
206333
const uint8_t debug_cell_row = 255, debug_cell_col = 255;
207334
if (row == debug_cell_row && col == debug_cell_col){
208335
debug_switch = true;
209336
cout << "start debugging switch at " << int(row) << ", " << int(col) << endl;
337+
PreloadSettings::debug().IMAGE_TEMPLATE_MATCHING = true;
210338
}
211-
bool detected = detect_at_cell(image_crop);
339+
const bool detected = detect_at_cell(screen.size(), image_crop);
212340
if (row == debug_cell_row && col == debug_cell_col){
213341
debug_switch = false;
342+
PreloadSettings::debug().IMAGE_TEMPLATE_MATCHING = false;
214343
}
215344
if (detected){
216345
if (arrow_found && m_debug_mode){
@@ -244,12 +373,13 @@ BoxCursorCoordinates BoxDetector::detected_location() const{
244373

245374
void BoxDetector::move_cursor(
246375
const ProgramInfo& info, VideoStream& stream, ProControllerContext& context,
247-
uint8_t row, uint8_t col
376+
uint8_t row, uint8_t col, bool holding_pokemon
248377
){
249378
if (row >= 6 || col >= 6){
250379
throw InternalProgramError(&stream.logger(), "BoxDetector::move_cursor",
251380
"row or col out of range: " + std::to_string(row) + ", " + std::to_string(col));
252381
}
382+
this->holding_pokemon(holding_pokemon);
253383

254384
WallClock start = current_time();
255385
while (true){

SerialPrograms/Source/PokemonLZA/Inference/Boxes/PokemonLZA_BoxDetection.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,23 @@ class BoxDetector : public StaticScreenDetector{
4343
public:
4444
BoxDetector(Color color = COLOR_RED, VideoOverlay* overlay = nullptr);
4545

46+
// Set whether the game is currently holding a pokemon to move around in box view.
47+
// The box cursor detection functionality inside BoxDetector needs to know this info.
48+
void holding_pokemon(bool holding_pokemon) { m_holding_pokemon = holding_pokemon; }
49+
4650
virtual void make_overlays(VideoOverlaySet& items) const override;
4751
virtual bool detect(const ImageViewRGB32& screen) override;
4852

4953
// return detected location fround by calling `detect()`
5054
BoxCursorCoordinates detected_location() const;
5155

5256
// While in the box system view, move the cursor to the desired slot.
57+
// - holding_pokemon: whether the movement is with a held pokemon
58+
// This function will call `holding_pokemon()` to change internal detection based on
59+
// whether the cursor is holding a pokemon or not.
5360
void move_cursor(
5461
const ProgramInfo& info, VideoStream& stream, ProControllerContext& context,
55-
uint8_t row, uint8_t col
62+
uint8_t row, uint8_t col, bool holding_pokemon = false
5663
);
5764

5865
// Under debug mode, will throw FatalProgramException when more than one box cell
@@ -61,12 +68,14 @@ class BoxDetector : public StaticScreenDetector{
6168

6269
private:
6370
// called for each box cell to check if the selection arrow is above that cell
64-
bool detect_at_cell(const ImageViewRGB32& image_crop);
71+
bool detect_at_cell(const Resolution& screen_resolution, const ImageViewRGB32& image_crop);
6572

73+
bool m_holding_pokemon = false;
6674
Color m_color;
6775

6876
ButtonDetector m_plus_button;
6977
std::vector<ImageFloatBox> m_arrow_boxes; // all 6 x 6 potential locations of the arrow interiors on box view
78+
std::vector<ImageFloatBox> m_lifted_arrow_boxes; // all 6 x 6 potential locations of the arrow interiors on box view when lifting/holding a pokemon
7079
uint8_t m_found_row = 0;
7180
uint8_t m_found_col = 0;
7281
bool m_debug_mode = false;

0 commit comments

Comments
 (0)