diff --git a/.github/workflows/cpp-ci-serial-programs.yml b/.github/workflows/cpp-ci-serial-programs.yml index 016a5a7fcc..c9faabd984 100644 --- a/.github/workflows/cpp-ci-serial-programs.yml +++ b/.github/workflows/cpp-ci-serial-programs.yml @@ -1,68 +1,68 @@ -name: C++ CI Serial Programs - -on: [push, pull_request, workflow_dispatch] - -jobs: - build: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - matrix: - os: [windows-2025, macos-13, ubuntu-24.04] - qt_version: ['6.9.0'] - include: - - qt_version: '6.9.0' - qt_version_major: '6' - qt_modules: 'qtmultimedia qtserialport' - - steps: - - uses: actions/checkout@v4 - with: - path: Arduino-Source - - uses: actions/checkout@v4 - with: - repository: 'PokemonAutomation/Packages' - path: Packages - - name: Install dependencies - if: startsWith(matrix.os, 'ubuntu') - run: | - sudo apt update - sudo apt install libopencv-dev - - name: Install dependencies - if: startsWith(matrix.os, 'mac') - run: | - brew install opencv onnxruntime - - uses: jurplel/install-qt-action@v4 - with: - version: ${{ matrix.qt_version }} - modules: ${{ matrix.qt_modules }} - - name: Generate binaries - run: | - cd Arduino-Source/SerialPrograms - mkdir bin - cd bin - cmake .. -DQT_MAJOR:STRING=${{ matrix.qt_version_major }} - cmake --build . --config Release --parallel 10 - - name: Copy resources - if: startsWith(matrix.os, 'windows') - run: | - robocopy Packages/SerialPrograms/Resources Output/Resources /s - robocopy Packages/PABotBase/PABotBase-Switch Output/PABotBase /s - robocopy Arduino-Source/SerialPrograms/bin Output/Binaries *.dll - robocopy Arduino-Source/SerialPrograms/bin/Release Output/Binaries SerialPrograms.exe - echo https://github.com/${{github.repository}}/commit/${{github.sha}} > Output/version.txt - write-host "Robocopy exited with exit code:" $lastexitcode - if ($lastexitcode -eq 1) - { - exit 0 - } - else - { - exit 1 - } - - uses: actions/upload-artifact@v4 - if: startsWith(matrix.os, 'windows') - with: - name: Serial Programs for windows (${{ matrix.qt_version }}) - path: Output +name: C++ CI Serial Programs + +on: [push, pull_request, workflow_dispatch] + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [windows-2025, macos-13, ubuntu-24.04] + qt_version: ['6.9.0'] + include: + - qt_version: '6.9.0' + qt_version_major: '6' + qt_modules: 'qtmultimedia qtserialport' + + steps: + - uses: actions/checkout@v4 + with: + path: Arduino-Source + - uses: actions/checkout@v4 + with: + repository: 'PokemonAutomation/Packages' + path: Packages + - name: Install dependencies + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt update + sudo apt install libopencv-dev + - name: Install dependencies + if: startsWith(matrix.os, 'mac') + run: | + brew install opencv onnxruntime + - uses: jurplel/install-qt-action@v4 + with: + version: ${{ matrix.qt_version }} + modules: ${{ matrix.qt_modules }} + - name: Generate binaries + run: | + cd Arduino-Source/SerialPrograms + mkdir bin + cd bin + cmake .. -DQT_MAJOR:STRING=${{ matrix.qt_version_major }} + cmake --build . --config Release --parallel 10 + - name: Copy resources + if: startsWith(matrix.os, 'windows') + run: | + robocopy Packages/SerialPrograms/Resources Output/Resources /s + robocopy Packages/PABotBase/PABotBase-Switch Output/PABotBase /s + robocopy Arduino-Source/SerialPrograms/bin Output/Binaries *.dll + robocopy Arduino-Source/SerialPrograms/bin/Release Output/Binaries SerialPrograms.exe + echo https://github.com/${{github.repository}}/commit/${{github.sha}} > Output/version.txt + write-host "Robocopy exited with exit code:" $lastexitcode + if ($lastexitcode -eq 1) + { + exit 0 + } + else + { + exit 1 + } + - uses: actions/upload-artifact@v4 + if: startsWith(matrix.os, 'windows') + with: + name: Serial Programs for windows (${{ matrix.qt_version }}) + path: Output diff --git a/.gitignore b/.gitignore index bc563f616b..7d8cd42553 100644 --- a/.gitignore +++ b/.gitignore @@ -1,57 +1,57 @@ -SerialPrograms/bin/ - -.vscode - -# macOS hidden system file -.DS_Store - -build-*/ -build/ - -# Qt config file on a user basis -*.pro.user - -# all other common C++ ignorables -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -SerialPrograms/*.so -SerialPrograms/*.dylib -SerialPrograms/*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -SerialPrograms/*.lai -SerialPrograms/*.la -SerialPrograms/*.a -SerialPrograms/*.lib - -# Executables -*.exe -*.out -*.app - -.vs -Resources/* -SerialPrograms/CMakeLists.txt.user -opencv_world4110d.dll - -# Python cache -__pycache__ - -# Jupyter Notebooks -*.ipynb +SerialPrograms/bin/ + +.vscode + +# macOS hidden system file +.DS_Store + +build-*/ +build/ + +# Qt config file on a user basis +*.pro.user + +# all other common C++ ignorables +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +SerialPrograms/*.so +SerialPrograms/*.dylib +SerialPrograms/*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +SerialPrograms/*.lai +SerialPrograms/*.la +SerialPrograms/*.a +SerialPrograms/*.lib + +# Executables +*.exe +*.out +*.app + +.vs +Resources/* +SerialPrograms/CMakeLists.txt.user +opencv_world4110d.dll + +# Python cache +__pycache__ + +# Jupyter Notebooks +*.ipynb diff --git a/IconResource/IconResource.rc b/IconResource/IconResource.rc index a841e99b45..e16ba4098e 100644 --- a/IconResource/IconResource.rc +++ b/IconResource/IconResource.rc @@ -1 +1 @@ -IDI_ICON1 ICON "icon.ico" +IDI_ICON1 ICON "icon.ico" diff --git a/LICENSE b/LICENSE index 495378a644..0b44e57762 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2021 Alexander J. Yee - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2021 Alexander J. Yee + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/SerialPrograms/Scripts/check_detector_regions.py b/SerialPrograms/Scripts/check_detector_regions.py index 2d535d6b33..34f6eeb071 100755 --- a/SerialPrograms/Scripts/check_detector_regions.py +++ b/SerialPrograms/Scripts/check_detector_regions.py @@ -1,253 +1,253 @@ -#!/usr/local/bin/python3 - -""" -A script used to fine-tune detection box placements and check the color stats from those boxes. -After using image_viewer.py to get an initial box placement, you can place the box values -(x, y, width, height) into this script, repeating the line -add_infer_box_to_image(raw_image, , , , , image) -to add a box onto the image. - -In the end of the script, it will call ImageViewer to render the image, with boxes added. -You can inspect the boxes to fine tune their positions. -""" - -import cv2 -import sys -import numpy as np -from typing import Tuple -from dataclasses import dataclass - -from image_viewer import ImageViewer -from image_region_check import RegionCheck, add_rect, add_infer_box_to_image - - -assert len(sys.argv) == 2 - -filename = sys.argv[1] - -image = cv2.imread(filename) -height = image.shape[0] -width = image.shape[1] -print(f"Size: {width} x {height}") - -raw_image = image.copy() - -# ================================================================== -# LA map weather symbol -# add_infer_box_to_image(raw_image, 0.028, 0.069, 0.025, 0.044, image) - -# ================================================================== -# LA mult-pokemon battle sprite arrow -# for i in range(8): -# loc = (0.936 - 0.035*i, 0.018, 0.015, 0.027) -# add_infer_box_to_image(raw_image, *loc, image) - -# ================================================================== -# LA battle sprite locations -# for i in range(8): -# loc = (0.957 - 0.035*i, 0.044, 0.021, 0.035) -# add_infer_box_to_image(raw_image, *loc, image) - -# ================================================================== -# LA battle + details pokemon info -# add_infer_box_to_image(raw_image, 0.726, 0.133, 0.015, 0.023, image) -# add_infer_box_to_image(raw_image, 0.750, 0.133, 0.015, 0.023, image) -# add_infer_box_to_image(raw_image, 0.777, 0.138, 0.001, 0.015, image) -# add_infer_box_to_image(raw_image, 0.525, 0.130, 0.100, 0.038, image) - -# ================================================================== -# LA wild pokemon focus reading pokemon info -# add_infer_box_to_image(raw_image, 0.108, 0.868, 0.135, 0.037, image) -# add_infer_box_to_image(raw_image, 0.307, 0.873, 0.016, 0.030, image) -# add_infer_box_to_image(raw_image, 0.307, 0.920, 0.016, 0.029, image) -# add_infer_box_to_image(raw_image, 0.324, 0.868, 0.023, 0.04, image) -# add_infer_box_to_image(raw_image, 0.244, 0.815, 0.026, 0.047, image) - - -# ================================================================== -# LA wild pokemon focus -# add_infer_box_to_image(raw_image, 0.109, 0.857, 0.24, 0.012, image) -# add_infer_box_to_image(raw_image, 0.109, 0.949, 0.24, 0.012, image) -# add_infer_box_to_image(raw_image, 0.102, 0.875, 0.007, 0.073, image) -# add_infer_box_to_image(raw_image, 0.348, 0.873, 0.007, 0.073, image) - -# ================================================================== -# LA battle start boundary lines -# add_infer_box_to_image(raw_image, 0.0, 0.113, 1.0, 0.015, image) -# add_infer_box_to_image(raw_image, 0.2, 0.871, 0.63, 0.015, image) - - -# ================================================================== -# LA map mission tab raised detection -# add_infer_box_to_image(raw_image, 0.9235, 0.617, 0.003, 0.019, image) -# add_infer_box_to_image(raw_image, 0.937, 0.62, 0.0035, 0.012, image) - -# ================================================================== -# LA map zoom -# add_infer_box_to_image(raw_image, 0.780, 0.085, 0.008, 0.014, image) -# add_infer_box_to_image(raw_image, 0.795, 0.082, 0.010, 0.019, image) -# add_infer_box_to_image(raw_image, 0.807, 0.081, 0.014, 0.022, image) - -# ================================================================== -# LA MMO Map question mark locations -# add_infer_box_to_image(raw_image, 0.362, 0.670, 0.045, 0.075, image) -# add_infer_box_to_image(raw_image, 0.683, 0.555, 0.039, 0.076, image) -# add_infer_box_to_image(raw_image, 0.828, 0.372, 0.042, 0.082, image) -# add_infer_box_to_image(raw_image, 0.485, 0.440, 0.044, 0.080, image) -# add_infer_box_to_image(raw_image, 0.393, 0.144, 0.050, 0.084, image) - -# ================================================================== -# LA Map location detector -# add_infer_box_to_image(raw_image, 0.252, 0.400, 0.025, 0.150, image) -# add_infer_box_to_image(raw_image, 0.415, 0.550, 0.025, 0.150, image) -# add_infer_box_to_image(raw_image, 0.750, 0.570, 0.025, 0.150, image) -# add_infer_box_to_image(raw_image, 0.865, 0.240, 0.025, 0.150, image) -# add_infer_box_to_image(raw_image, 0.508, 0.320, 0.025, 0.150, image) -# add_infer_box_to_image(raw_image, 0.457, 0.060, 0.025, 0.150, image) -# add_infer_box_to_image(raw_image, 0.635, 0.285, 0.025, 0.150, image) - - -# ================================================================== -# LA dropped items detection -# add_infer_box_to_image(raw_image, 0.030, 0.177, 0.020, 0.038, image) -# add_infer_box_to_image(raw_image, 0.030, 0.225, 0.020, 0.038, image) -# add_infer_box_to_image(raw_image, 0.050, 0.177, 0.200, 0.038, image) -# add_infer_box_to_image(raw_image, 0.050, 0.177, 0.055, 0.038, image) # MMO ?????? - -# ================================================================== -# LA dropped items detection -# add_infer_box_to_image(raw_image, 0.439, 0.819, 0.029, 0.059, image) - -# add_infer_box_to_image(raw_image, 0.031, 0.900, 0.024, 0.017, image) -# add_infer_box_to_image(raw_image, 0.026, 0.743, 0.021, 0.038, image) -# add_infer_box_to_image(raw_image, 0.022, 0.581, 0.020, 0.018, image) -# add_infer_box_to_image(raw_image, 0.152, 0.482, 0.017, 0.040, image) -# add_infer_box_to_image(raw_image, 0.016, 0.218, 0.018, 0.021, image) -# add_infer_box_to_image(raw_image, 0.169, 0.103, 0.024, 0.036, image) -# add_infer_box_to_image(raw_image, 0.815, 0.244, 0.026, 0.026, image) -# add_infer_box_to_image(raw_image, 0.737, 0.126, 0.045, 0.056, image) -# add_infer_box_to_image(raw_image, 0.806, 0.431, 0.043, 0.026, image) -# add_infer_box_to_image(raw_image, 0.827, 0.507, 0.023, 0.028, image) -# add_infer_box_to_image(raw_image, 0.747, 0.603, 0.087, 0.086, image) -# add_infer_box_to_image(raw_image, 0.885, 0.653, 0.041, 0.093, image) -# add_infer_box_to_image(raw_image, 0.934, 0.556, 0.039, 0.038, image) -# add_infer_box_to_image(raw_image, 0.761, 0.913, 0.071, 0.051, image) -# add_infer_box_to_image(raw_image, 0.865, 0.098, 0.022, 0.030, image) -# add_infer_box_to_image(raw_image, 0.202, 0.631, 0.057, 0.075, image) -# add_infer_box_to_image(raw_image, 0.099, 0.265, 0.072, 0.070, image) -# add_infer_box_to_image(raw_image, 0.908, 0.313, 0.020, 0.017, image) - -# ================================================================== -# LA black out detection -# add_infer_box_to_image(raw_image, 0.068, 0.088, 0.864, 0.581, image) -# add_infer_box_to_image(raw_image, 0.720, 0.842, 0.028, 0.051, image) - - -# ================================================================== -# LA Transparent Dialogue Yellow arrow detection -# add_infer_box_to_image(raw_image, 0.720, 0.759, 0.049, 0.128, image) - -# ================================================================== -# LA Dialogue Ellipse detection -# add_infer_box_to_image(raw_image, 0.741, 0.811, 0.028, 0.023, image) - -# ================================================================== -# LA Tenacity path menu -# add_infer_box_to_image(raw_image, 0.56, 0.46, 0.33, 0.27, image) - -# ================================================================== -# LA wild pokemon name in focus -# add_infer_box_to_image(raw_image, 0.11, 0.868, 0.135, 0.043, image) - -# ================================================================== -# LA camp dialogue box -# add_infer_box_to_image(raw_image, 0.741, 0.811, 0.028, 0.023, image) - -# ================================================================== -# LA item compatibility -# add_infer_box_to_image(raw_image, 0.838, 0.1815, 0.090, 0.024, image) - -# ================================================================== -# LA battle move image match -# for i in range(4): - # add_infer_box_to_image(raw_image, 0.66 - i * 0.0205, 0.622 + i * 0.0655, 0.25, 0.032, image) - -# ================================================================== -# LA Ingo battle initial dialogue selection -# add_infer_box_to_image(raw_image, 0.50, 0.350, 0.40, 0.400, image) -# add_infer_box_to_image(raw_image, 0.50, 0.350, 0.40, 0.400, image) - - -# ================================================================== -# LA battle pokemon switch -# add_infer_box_to_image(raw_image, 0.641, 0.178, 0.050, 0.023, image) -# add_infer_box_to_image(raw_image, 0.641, 0.248, 0.050, 0.023, image) -# add_infer_box_to_image(raw_image, 0.517, 0.195, 0.011, 0.061, image) -# add_infer_box_to_image(raw_image, 0.924, 0.185, 0.019, 0.076, image) - -# add_infer_box_to_image(raw_image, 0.540, 0.216, 0.016, 0.018, image) -# add_infer_box_to_image(raw_image, 0.676, 0.216, 0.016, 0.018, image) - -# add_infer_box_to_image(raw_image, 0.044, 0.091, 0.043, 0.077, image) - - -# ================================================================== -# LA battle move selection -# for i in range(4): - # add_infer_box_to_image(raw_image, 0.8 - i * 0.021, 0.622 + i * 0.0655, 0.02, 0.032, image) - -# ================================================================== -# LA battle menu -# add_infer_box_to_image(raw_image, 0.056, 0.948, 0.013, 0.020, image) -# add_infer_box_to_image(raw_image, 0.174, 0.948, 0.032, 0.046, image) - -# ================================================================== -# LA normal opaque dialogue -# add_infer_box_to_image(raw_image, 0.278, 0.712, 0.100, 0.005, image) -# add_infer_box_to_image(raw_image, 0.278, 0.755, 0.100, 0.005, image) -# add_infer_box_to_image(raw_image, 0.259, 0.715, 0.003, 0.043, image) -# add_infer_box_to_image(raw_image, 0.390, 0.715, 0.003, 0.043, image) - -# add_infer_box_to_image(raw_image, 0.500, 0.750, 0.200, 0.020, image) -# add_infer_box_to_image(raw_image, 0.400, 0.895, 0.200, 0.020, image) -# add_infer_box_to_image(raw_image, 0.230, 0.805, 0.016, 0.057, image) -# add_infer_box_to_image(raw_image, 0.755, 0.805, 0.016, 0.057, image) - -# ================================================================== -# LA normal surprise dialogue -# add_infer_box_to_image(raw_image, 0.295, 0.722, 0.100, 0.005, image) -# add_infer_box_to_image(raw_image, 0.295, 0.765, 0.100, 0.005, image) - -# add_infer_box_to_image(raw_image, 0.500, 0.760, 0.200, 0.020, image) -# add_infer_box_to_image(raw_image, 0.400, 0.900, 0.200, 0.020, image) -# add_infer_box_to_image(raw_image, 0.720, 0.855, 0.030, 0.060, image) - -# ================================================================== -# Pokemon Home Box Sorting - -# , m_box_sprite(console, {0.228, 0.095, 0.030, 0.049}) // ball -# ImageFloatBox GENDER_BOX{0.417, 0.097, 0.031, 0.046}; // gender - -# add_infer_box_to_image(raw_image, 0.495, 0.0045, 0.01, 0.005, image) # square color to check which mode is active -# add_infer_box_to_image(raw_image, 0.447, 0.250, 0.037, 0.034, image) # pokemon national dex number pos -# add_infer_box_to_image(raw_image, 0.702, 0.09, 0.04, 0.06, image) # shiny symbol pos -# add_infer_box_to_image(raw_image, 0.463, 0.09, 0.04, 0.06, image) # gmax symbol pos -# add_infer_box_to_image(raw_image, 0.623, 0.095, 0.033, 0.05, image) # origin symbol pos -# add_infer_box_to_image(raw_image, 0.69, 0.18, 0.28, 0.46, image) # pokemon render pos -# add_infer_box_to_image(raw_image, 0.228, 0.095, 0.030, 0.049, image) # ball type pos -# add_infer_box_to_image(raw_image, 0.417, 0.097, 0.031, 0.046, image) # gender pos -# add_infer_box_to_image(raw_image, 0.546, 0.099, 0.044, 0.041, image) # Level box -# add_infer_box_to_image(raw_image, 0.782, 0.719, 0.193, 0.046, image) # OT ID box -# add_infer_box_to_image(raw_image, 0.492, 0.719, 0.165, 0.049, image) # OT box -# add_infer_box_to_image(raw_image, 0.157, 0.783, 0.212, 0.042, image) # Nature box -# add_infer_box_to_image(raw_image, 0.158, 0.838, 0.213, 0.042, image) # Ability box - -# ================================================================== -# Pokemon BDSP Poffin Cooking - -# add_infer_box_to_image(raw_image, 0.56, 0.724, 0.012, 0.024, image) # green & blue arrow pos - - -viewer = ImageViewer(image) -viewer.run() - +#!/usr/local/bin/python3 + +""" +A script used to fine-tune detection box placements and check the color stats from those boxes. +After using image_viewer.py to get an initial box placement, you can place the box values +(x, y, width, height) into this script, repeating the line +add_infer_box_to_image(raw_image, , , , , image) +to add a box onto the image. + +In the end of the script, it will call ImageViewer to render the image, with boxes added. +You can inspect the boxes to fine tune their positions. +""" + +import cv2 +import sys +import numpy as np +from typing import Tuple +from dataclasses import dataclass + +from image_viewer import ImageViewer +from image_region_check import RegionCheck, add_rect, add_infer_box_to_image + + +assert len(sys.argv) == 2 + +filename = sys.argv[1] + +image = cv2.imread(filename) +height = image.shape[0] +width = image.shape[1] +print(f"Size: {width} x {height}") + +raw_image = image.copy() + +# ================================================================== +# LA map weather symbol +# add_infer_box_to_image(raw_image, 0.028, 0.069, 0.025, 0.044, image) + +# ================================================================== +# LA mult-pokemon battle sprite arrow +# for i in range(8): +# loc = (0.936 - 0.035*i, 0.018, 0.015, 0.027) +# add_infer_box_to_image(raw_image, *loc, image) + +# ================================================================== +# LA battle sprite locations +# for i in range(8): +# loc = (0.957 - 0.035*i, 0.044, 0.021, 0.035) +# add_infer_box_to_image(raw_image, *loc, image) + +# ================================================================== +# LA battle + details pokemon info +# add_infer_box_to_image(raw_image, 0.726, 0.133, 0.015, 0.023, image) +# add_infer_box_to_image(raw_image, 0.750, 0.133, 0.015, 0.023, image) +# add_infer_box_to_image(raw_image, 0.777, 0.138, 0.001, 0.015, image) +# add_infer_box_to_image(raw_image, 0.525, 0.130, 0.100, 0.038, image) + +# ================================================================== +# LA wild pokemon focus reading pokemon info +# add_infer_box_to_image(raw_image, 0.108, 0.868, 0.135, 0.037, image) +# add_infer_box_to_image(raw_image, 0.307, 0.873, 0.016, 0.030, image) +# add_infer_box_to_image(raw_image, 0.307, 0.920, 0.016, 0.029, image) +# add_infer_box_to_image(raw_image, 0.324, 0.868, 0.023, 0.04, image) +# add_infer_box_to_image(raw_image, 0.244, 0.815, 0.026, 0.047, image) + + +# ================================================================== +# LA wild pokemon focus +# add_infer_box_to_image(raw_image, 0.109, 0.857, 0.24, 0.012, image) +# add_infer_box_to_image(raw_image, 0.109, 0.949, 0.24, 0.012, image) +# add_infer_box_to_image(raw_image, 0.102, 0.875, 0.007, 0.073, image) +# add_infer_box_to_image(raw_image, 0.348, 0.873, 0.007, 0.073, image) + +# ================================================================== +# LA battle start boundary lines +# add_infer_box_to_image(raw_image, 0.0, 0.113, 1.0, 0.015, image) +# add_infer_box_to_image(raw_image, 0.2, 0.871, 0.63, 0.015, image) + + +# ================================================================== +# LA map mission tab raised detection +# add_infer_box_to_image(raw_image, 0.9235, 0.617, 0.003, 0.019, image) +# add_infer_box_to_image(raw_image, 0.937, 0.62, 0.0035, 0.012, image) + +# ================================================================== +# LA map zoom +# add_infer_box_to_image(raw_image, 0.780, 0.085, 0.008, 0.014, image) +# add_infer_box_to_image(raw_image, 0.795, 0.082, 0.010, 0.019, image) +# add_infer_box_to_image(raw_image, 0.807, 0.081, 0.014, 0.022, image) + +# ================================================================== +# LA MMO Map question mark locations +# add_infer_box_to_image(raw_image, 0.362, 0.670, 0.045, 0.075, image) +# add_infer_box_to_image(raw_image, 0.683, 0.555, 0.039, 0.076, image) +# add_infer_box_to_image(raw_image, 0.828, 0.372, 0.042, 0.082, image) +# add_infer_box_to_image(raw_image, 0.485, 0.440, 0.044, 0.080, image) +# add_infer_box_to_image(raw_image, 0.393, 0.144, 0.050, 0.084, image) + +# ================================================================== +# LA Map location detector +# add_infer_box_to_image(raw_image, 0.252, 0.400, 0.025, 0.150, image) +# add_infer_box_to_image(raw_image, 0.415, 0.550, 0.025, 0.150, image) +# add_infer_box_to_image(raw_image, 0.750, 0.570, 0.025, 0.150, image) +# add_infer_box_to_image(raw_image, 0.865, 0.240, 0.025, 0.150, image) +# add_infer_box_to_image(raw_image, 0.508, 0.320, 0.025, 0.150, image) +# add_infer_box_to_image(raw_image, 0.457, 0.060, 0.025, 0.150, image) +# add_infer_box_to_image(raw_image, 0.635, 0.285, 0.025, 0.150, image) + + +# ================================================================== +# LA dropped items detection +# add_infer_box_to_image(raw_image, 0.030, 0.177, 0.020, 0.038, image) +# add_infer_box_to_image(raw_image, 0.030, 0.225, 0.020, 0.038, image) +# add_infer_box_to_image(raw_image, 0.050, 0.177, 0.200, 0.038, image) +# add_infer_box_to_image(raw_image, 0.050, 0.177, 0.055, 0.038, image) # MMO ?????? + +# ================================================================== +# LA dropped items detection +# add_infer_box_to_image(raw_image, 0.439, 0.819, 0.029, 0.059, image) + +# add_infer_box_to_image(raw_image, 0.031, 0.900, 0.024, 0.017, image) +# add_infer_box_to_image(raw_image, 0.026, 0.743, 0.021, 0.038, image) +# add_infer_box_to_image(raw_image, 0.022, 0.581, 0.020, 0.018, image) +# add_infer_box_to_image(raw_image, 0.152, 0.482, 0.017, 0.040, image) +# add_infer_box_to_image(raw_image, 0.016, 0.218, 0.018, 0.021, image) +# add_infer_box_to_image(raw_image, 0.169, 0.103, 0.024, 0.036, image) +# add_infer_box_to_image(raw_image, 0.815, 0.244, 0.026, 0.026, image) +# add_infer_box_to_image(raw_image, 0.737, 0.126, 0.045, 0.056, image) +# add_infer_box_to_image(raw_image, 0.806, 0.431, 0.043, 0.026, image) +# add_infer_box_to_image(raw_image, 0.827, 0.507, 0.023, 0.028, image) +# add_infer_box_to_image(raw_image, 0.747, 0.603, 0.087, 0.086, image) +# add_infer_box_to_image(raw_image, 0.885, 0.653, 0.041, 0.093, image) +# add_infer_box_to_image(raw_image, 0.934, 0.556, 0.039, 0.038, image) +# add_infer_box_to_image(raw_image, 0.761, 0.913, 0.071, 0.051, image) +# add_infer_box_to_image(raw_image, 0.865, 0.098, 0.022, 0.030, image) +# add_infer_box_to_image(raw_image, 0.202, 0.631, 0.057, 0.075, image) +# add_infer_box_to_image(raw_image, 0.099, 0.265, 0.072, 0.070, image) +# add_infer_box_to_image(raw_image, 0.908, 0.313, 0.020, 0.017, image) + +# ================================================================== +# LA black out detection +# add_infer_box_to_image(raw_image, 0.068, 0.088, 0.864, 0.581, image) +# add_infer_box_to_image(raw_image, 0.720, 0.842, 0.028, 0.051, image) + + +# ================================================================== +# LA Transparent Dialogue Yellow arrow detection +# add_infer_box_to_image(raw_image, 0.720, 0.759, 0.049, 0.128, image) + +# ================================================================== +# LA Dialogue Ellipse detection +# add_infer_box_to_image(raw_image, 0.741, 0.811, 0.028, 0.023, image) + +# ================================================================== +# LA Tenacity path menu +# add_infer_box_to_image(raw_image, 0.56, 0.46, 0.33, 0.27, image) + +# ================================================================== +# LA wild pokemon name in focus +# add_infer_box_to_image(raw_image, 0.11, 0.868, 0.135, 0.043, image) + +# ================================================================== +# LA camp dialogue box +# add_infer_box_to_image(raw_image, 0.741, 0.811, 0.028, 0.023, image) + +# ================================================================== +# LA item compatibility +# add_infer_box_to_image(raw_image, 0.838, 0.1815, 0.090, 0.024, image) + +# ================================================================== +# LA battle move image match +# for i in range(4): + # add_infer_box_to_image(raw_image, 0.66 - i * 0.0205, 0.622 + i * 0.0655, 0.25, 0.032, image) + +# ================================================================== +# LA Ingo battle initial dialogue selection +# add_infer_box_to_image(raw_image, 0.50, 0.350, 0.40, 0.400, image) +# add_infer_box_to_image(raw_image, 0.50, 0.350, 0.40, 0.400, image) + + +# ================================================================== +# LA battle pokemon switch +# add_infer_box_to_image(raw_image, 0.641, 0.178, 0.050, 0.023, image) +# add_infer_box_to_image(raw_image, 0.641, 0.248, 0.050, 0.023, image) +# add_infer_box_to_image(raw_image, 0.517, 0.195, 0.011, 0.061, image) +# add_infer_box_to_image(raw_image, 0.924, 0.185, 0.019, 0.076, image) + +# add_infer_box_to_image(raw_image, 0.540, 0.216, 0.016, 0.018, image) +# add_infer_box_to_image(raw_image, 0.676, 0.216, 0.016, 0.018, image) + +# add_infer_box_to_image(raw_image, 0.044, 0.091, 0.043, 0.077, image) + + +# ================================================================== +# LA battle move selection +# for i in range(4): + # add_infer_box_to_image(raw_image, 0.8 - i * 0.021, 0.622 + i * 0.0655, 0.02, 0.032, image) + +# ================================================================== +# LA battle menu +# add_infer_box_to_image(raw_image, 0.056, 0.948, 0.013, 0.020, image) +# add_infer_box_to_image(raw_image, 0.174, 0.948, 0.032, 0.046, image) + +# ================================================================== +# LA normal opaque dialogue +# add_infer_box_to_image(raw_image, 0.278, 0.712, 0.100, 0.005, image) +# add_infer_box_to_image(raw_image, 0.278, 0.755, 0.100, 0.005, image) +# add_infer_box_to_image(raw_image, 0.259, 0.715, 0.003, 0.043, image) +# add_infer_box_to_image(raw_image, 0.390, 0.715, 0.003, 0.043, image) + +# add_infer_box_to_image(raw_image, 0.500, 0.750, 0.200, 0.020, image) +# add_infer_box_to_image(raw_image, 0.400, 0.895, 0.200, 0.020, image) +# add_infer_box_to_image(raw_image, 0.230, 0.805, 0.016, 0.057, image) +# add_infer_box_to_image(raw_image, 0.755, 0.805, 0.016, 0.057, image) + +# ================================================================== +# LA normal surprise dialogue +# add_infer_box_to_image(raw_image, 0.295, 0.722, 0.100, 0.005, image) +# add_infer_box_to_image(raw_image, 0.295, 0.765, 0.100, 0.005, image) + +# add_infer_box_to_image(raw_image, 0.500, 0.760, 0.200, 0.020, image) +# add_infer_box_to_image(raw_image, 0.400, 0.900, 0.200, 0.020, image) +# add_infer_box_to_image(raw_image, 0.720, 0.855, 0.030, 0.060, image) + +# ================================================================== +# Pokemon Home Box Sorting + +# , m_box_sprite(console, {0.228, 0.095, 0.030, 0.049}) // ball +# ImageFloatBox GENDER_BOX{0.417, 0.097, 0.031, 0.046}; // gender + +# add_infer_box_to_image(raw_image, 0.495, 0.0045, 0.01, 0.005, image) # square color to check which mode is active +# add_infer_box_to_image(raw_image, 0.447, 0.250, 0.037, 0.034, image) # pokemon national dex number pos +# add_infer_box_to_image(raw_image, 0.702, 0.09, 0.04, 0.06, image) # shiny symbol pos +# add_infer_box_to_image(raw_image, 0.463, 0.09, 0.04, 0.06, image) # gmax symbol pos +# add_infer_box_to_image(raw_image, 0.623, 0.095, 0.033, 0.05, image) # origin symbol pos +# add_infer_box_to_image(raw_image, 0.69, 0.18, 0.28, 0.46, image) # pokemon render pos +# add_infer_box_to_image(raw_image, 0.228, 0.095, 0.030, 0.049, image) # ball type pos +# add_infer_box_to_image(raw_image, 0.417, 0.097, 0.031, 0.046, image) # gender pos +# add_infer_box_to_image(raw_image, 0.546, 0.099, 0.044, 0.041, image) # Level box +# add_infer_box_to_image(raw_image, 0.782, 0.719, 0.193, 0.046, image) # OT ID box +# add_infer_box_to_image(raw_image, 0.492, 0.719, 0.165, 0.049, image) # OT box +# add_infer_box_to_image(raw_image, 0.157, 0.783, 0.212, 0.042, image) # Nature box +# add_infer_box_to_image(raw_image, 0.158, 0.838, 0.213, 0.042, image) # Ability box + +# ================================================================== +# Pokemon BDSP Poffin Cooking + +# add_infer_box_to_image(raw_image, 0.56, 0.724, 0.012, 0.024, image) # green & blue arrow pos + + +viewer = ImageViewer(image) +viewer.run() + diff --git a/SerialPrograms/Scripts/image_region_check.py b/SerialPrograms/Scripts/image_region_check.py index 180d16a2fa..9f96b5d300 100644 --- a/SerialPrograms/Scripts/image_region_check.py +++ b/SerialPrograms/Scripts/image_region_check.py @@ -1,175 +1,175 @@ -""" -Functions to check stats on image regions: what's the average color of this region and the std dev of the color of this region. -These stats is useful for image inferences like whether the current screen shows a pokemon battle menu. -The code here matches the implementation in C++, so that we can quickly prototype and debug image inference code in Python. -""" -import cv2 -import sys -import numpy as np -from typing import Tuple, Union, List, NamedTuple -from dataclasses import dataclass - -class Stats(NamedTuple): - # RGB order in this class - - avg: np.ndarray - - avg_sum: float - - stddev: np.ndarray - - stddev_sum: float - - color_ratio: np.ndarray - - # Crop size: width, height - crop_size: Tuple[int, int] - - def to_str(self) -> str: - avg = self.avg - ratio = self.color_ratio - stdev = self.stddev - return ( - f"RGB avg [{int(avg[0]+0.5)}, {int(avg[1]+0.5)}, {int(avg[2]+0.5)}]" - + f" avg sum {int(self.avg_sum+0.5)} ratio [{ratio[0]:.3f}, {ratio[1]:.3f}, {ratio[2]:.3f}]" - + f" stddev [{stdev[0]:.3f}, {stdev[1]:.3f}, {stdev[2]:.3f}] sum {self.stddev_sum:.3f}" - + f" crop size {self.crop_size}" - ) - - - -def _get_region(image: np.ndarray, x: float, y: float, w: float, h: float) -> Tuple[int, int, int, int]: - """ - Given an image and (x, y, w, h) denoting a resolution-independent region, return - the actual region in pixel counts. - """ - height = image.shape[0] - width = image.shape[1] - start_x = int(x * width + 0.5) - start_y = int(y * height + 0.5) - rect_width = int(w * width + 0.5) - rect_height = int(h * height + 0.5) - end_x = start_x + rect_width - end_y = start_y + rect_height - return start_x, start_y, end_x, end_y - - -def add_rect(image: np.ndarray, x: float, y: float, w: float, h: float, color=(0, 0, 255)) -> np.ndarray: - """ - Add a red rectangle around a region, where (x, y, w, h) denoting a resolution-independent region. - """ - start_x, start_y, end_x, end_y = _get_region(image, x, y, w, h) - # print(start_x, start_y, end_x, end_y) - image = cv2.rectangle(image, (start_x, start_y), (end_x, end_y), color, 2) - return image - -def _get_stats(image: np.ndarray, x: float, y: float, w: float, h: float) -> Stats: - """ - Given (x, y, w, h) denoting a resolution-independent region, return the color stats of the region. - - Return the average color of the region and the std dev of the region. - """ - start_x, start_y, end_x, end_y = _get_region(image, x, y, w, h) - crop = image[start_y:end_y, start_x:end_x].astype(float) - num_pixels = crop.shape[0] * crop.shape[1] - crop_sum = np.sum(crop, (0, 1)) - crop_avg = crop_sum / num_pixels - avg_sum = np.sum(crop_avg) - crop_color_ratio = [1/3., 1/3., 1/3.] if avg_sum <= 1e-6 else crop_avg / avg_sum - crop_color_ratio = np.array([crop_color_ratio[2], crop_color_ratio[1], crop_color_ratio[0]]) - - crop_sqr_sum = np.sum(np.square(crop), (0, 1)) - crop_stddev = np.sqrt((crop_sqr_sum - (np.square(crop_sum) / num_pixels)) / (num_pixels-1)) - crop_stddev_sum = np.sum(crop_stddev) - return Stats( - avg = np.flip(crop_avg), # from BGR to RGB - avg_sum = avg_sum, - stddev = np.flip(crop_stddev), # from BGR to RGB - stddev_sum = crop_stddev_sum, - color_ratio = crop_color_ratio, - crop_size=(end_x - start_x, end_y - start_y), - ) - -def add_infer_box_to_image( - image: np.ndarray, - x: float, y: float, w: float, h: float, - rendered_image: np.ndarray, - color=(0,0,255) -) -> np.ndarray: - stats = _get_stats(image, x, y, w, h) - ratio = stats.color_ratio - print(f"Add infer box: ({x:0.4f}, {y:0.4f}, {w:0.4f}, {h:0.4f}), {stats.to_str()}") - return add_rect(rendered_image, x, y, w, h, color) - -def _color_matched( - crop_color_ratio: np.ndarray, - crop_stddev_sum: float, - expected_color_ratio: Union[np.ndarray, Tuple[float, float, float]], - max_euclidean_distance: float, - max_stddev_sum: float -) -> bool: - """Check whether an inference box check is matched""" - dist = crop_color_ratio - np.array(expected_color_ratio) - dist = np.square(dist) - euclidean_distance = np.sqrt(np.sum(dist)) - - # print(f"stddev_sum: {stddev_sum} vs max {max_stddev_sum}, rgb actual: {actual}, eu_dist: {euclidean_distance} vs max {max_euclidean_distance}") - if crop_stddev_sum > max_stddev_sum: - return False - return euclidean_distance <= max_euclidean_distance - - -@dataclass -class RegionCheck: - # Name of the region - name: str - # (x, y, w, h) denoting a resolution-independent region - region: Tuple[float, float, float, float] - # extpected the color ratio, all three values sum to one - expected_color_ratio: Tuple[float, float, float] - # threshold for euclidendistance between the color aspect ratio (size-3 vecctor) of - # the current image and the expected ratio. - max_euclidean_distance: float - # threshold for sum of stddev from the three color channels. - max_stddev_sum: float - - def check(self, image: np.ndarray) -> bool: - """Check whether this image passes inference check.""" - stats = _get_stats(image, *self.region) - crop_ratio = stats.color_ratio - crop_stddev_sum = stats.stddev_sum - print(f"Checking {self.name}") - print(stats.to_str()) - solid = _color_matched(crop_ratio, crop_stddev_sum, self.expected_color_ratio, self.max_euclidean_distance, self.max_stddev_sum) - print(f"_color_matched? {solid}") - return solid - - -def check_image(filename: str, regions: List[RegionCheck]) -> None: - """Print whether an image saved in filename passes all region checks.""" - image = cv2.imread(filename, cv2.IMREAD_UNCHANGED) - height = image.shape[0] - width = image.shape[1] - print(f"Load image {filename}, size: {width} x {height}") - - for region in regions: - if region.check(image) == False: - print("====== Failed ======") - return - print("====== Passed ======") - - -def set_black_out_of_rect(image: np.ndarray, x: float, y: float, w: float, h: float): - """Set regions outside of the rect on `image` to be black.""" - height = image.shape[0] - width = image.shape[1] - start_x = int(x * width + 0.5) - start_y = int(y * height + 0.5) - rect_width = int(w * width + 0.5) - rect_height = int(h * height + 0.5) - end_x = start_x + rect_width - end_y = start_y + rect_height - - new_image = np.zeros(image.shape, dtype=image.dtype) - new_image[start_y:end_y, start_x:end_x] = image[start_y:end_y, start_x:end_x] +""" +Functions to check stats on image regions: what's the average color of this region and the std dev of the color of this region. +These stats is useful for image inferences like whether the current screen shows a pokemon battle menu. +The code here matches the implementation in C++, so that we can quickly prototype and debug image inference code in Python. +""" +import cv2 +import sys +import numpy as np +from typing import Tuple, Union, List, NamedTuple +from dataclasses import dataclass + +class Stats(NamedTuple): + # RGB order in this class + + avg: np.ndarray + + avg_sum: float + + stddev: np.ndarray + + stddev_sum: float + + color_ratio: np.ndarray + + # Crop size: width, height + crop_size: Tuple[int, int] + + def to_str(self) -> str: + avg = self.avg + ratio = self.color_ratio + stdev = self.stddev + return ( + f"RGB avg [{int(avg[0]+0.5)}, {int(avg[1]+0.5)}, {int(avg[2]+0.5)}]" + + f" avg sum {int(self.avg_sum+0.5)} ratio [{ratio[0]:.3f}, {ratio[1]:.3f}, {ratio[2]:.3f}]" + + f" stddev [{stdev[0]:.3f}, {stdev[1]:.3f}, {stdev[2]:.3f}] sum {self.stddev_sum:.3f}" + + f" crop size {self.crop_size}" + ) + + + +def _get_region(image: np.ndarray, x: float, y: float, w: float, h: float) -> Tuple[int, int, int, int]: + """ + Given an image and (x, y, w, h) denoting a resolution-independent region, return + the actual region in pixel counts. + """ + height = image.shape[0] + width = image.shape[1] + start_x = int(x * width + 0.5) + start_y = int(y * height + 0.5) + rect_width = int(w * width + 0.5) + rect_height = int(h * height + 0.5) + end_x = start_x + rect_width + end_y = start_y + rect_height + return start_x, start_y, end_x, end_y + + +def add_rect(image: np.ndarray, x: float, y: float, w: float, h: float, color=(0, 0, 255)) -> np.ndarray: + """ + Add a red rectangle around a region, where (x, y, w, h) denoting a resolution-independent region. + """ + start_x, start_y, end_x, end_y = _get_region(image, x, y, w, h) + # print(start_x, start_y, end_x, end_y) + image = cv2.rectangle(image, (start_x, start_y), (end_x, end_y), color, 2) + return image + +def _get_stats(image: np.ndarray, x: float, y: float, w: float, h: float) -> Stats: + """ + Given (x, y, w, h) denoting a resolution-independent region, return the color stats of the region. + + Return the average color of the region and the std dev of the region. + """ + start_x, start_y, end_x, end_y = _get_region(image, x, y, w, h) + crop = image[start_y:end_y, start_x:end_x].astype(float) + num_pixels = crop.shape[0] * crop.shape[1] + crop_sum = np.sum(crop, (0, 1)) + crop_avg = crop_sum / num_pixels + avg_sum = np.sum(crop_avg) + crop_color_ratio = [1/3., 1/3., 1/3.] if avg_sum <= 1e-6 else crop_avg / avg_sum + crop_color_ratio = np.array([crop_color_ratio[2], crop_color_ratio[1], crop_color_ratio[0]]) + + crop_sqr_sum = np.sum(np.square(crop), (0, 1)) + crop_stddev = np.sqrt((crop_sqr_sum - (np.square(crop_sum) / num_pixels)) / (num_pixels-1)) + crop_stddev_sum = np.sum(crop_stddev) + return Stats( + avg = np.flip(crop_avg), # from BGR to RGB + avg_sum = avg_sum, + stddev = np.flip(crop_stddev), # from BGR to RGB + stddev_sum = crop_stddev_sum, + color_ratio = crop_color_ratio, + crop_size=(end_x - start_x, end_y - start_y), + ) + +def add_infer_box_to_image( + image: np.ndarray, + x: float, y: float, w: float, h: float, + rendered_image: np.ndarray, + color=(0,0,255) +) -> np.ndarray: + stats = _get_stats(image, x, y, w, h) + ratio = stats.color_ratio + print(f"Add infer box: ({x:0.4f}, {y:0.4f}, {w:0.4f}, {h:0.4f}), {stats.to_str()}") + return add_rect(rendered_image, x, y, w, h, color) + +def _color_matched( + crop_color_ratio: np.ndarray, + crop_stddev_sum: float, + expected_color_ratio: Union[np.ndarray, Tuple[float, float, float]], + max_euclidean_distance: float, + max_stddev_sum: float +) -> bool: + """Check whether an inference box check is matched""" + dist = crop_color_ratio - np.array(expected_color_ratio) + dist = np.square(dist) + euclidean_distance = np.sqrt(np.sum(dist)) + + # print(f"stddev_sum: {stddev_sum} vs max {max_stddev_sum}, rgb actual: {actual}, eu_dist: {euclidean_distance} vs max {max_euclidean_distance}") + if crop_stddev_sum > max_stddev_sum: + return False + return euclidean_distance <= max_euclidean_distance + + +@dataclass +class RegionCheck: + # Name of the region + name: str + # (x, y, w, h) denoting a resolution-independent region + region: Tuple[float, float, float, float] + # extpected the color ratio, all three values sum to one + expected_color_ratio: Tuple[float, float, float] + # threshold for euclidendistance between the color aspect ratio (size-3 vecctor) of + # the current image and the expected ratio. + max_euclidean_distance: float + # threshold for sum of stddev from the three color channels. + max_stddev_sum: float + + def check(self, image: np.ndarray) -> bool: + """Check whether this image passes inference check.""" + stats = _get_stats(image, *self.region) + crop_ratio = stats.color_ratio + crop_stddev_sum = stats.stddev_sum + print(f"Checking {self.name}") + print(stats.to_str()) + solid = _color_matched(crop_ratio, crop_stddev_sum, self.expected_color_ratio, self.max_euclidean_distance, self.max_stddev_sum) + print(f"_color_matched? {solid}") + return solid + + +def check_image(filename: str, regions: List[RegionCheck]) -> None: + """Print whether an image saved in filename passes all region checks.""" + image = cv2.imread(filename, cv2.IMREAD_UNCHANGED) + height = image.shape[0] + width = image.shape[1] + print(f"Load image {filename}, size: {width} x {height}") + + for region in regions: + if region.check(image) == False: + print("====== Failed ======") + return + print("====== Passed ======") + + +def set_black_out_of_rect(image: np.ndarray, x: float, y: float, w: float, h: float): + """Set regions outside of the rect on `image` to be black.""" + height = image.shape[0] + width = image.shape[1] + start_x = int(x * width + 0.5) + start_y = int(y * height + 0.5) + rect_width = int(w * width + 0.5) + rect_height = int(h * height + 0.5) + end_x = start_x + rect_width + end_y = start_y + rect_height + + new_image = np.zeros(image.shape, dtype=image.dtype) + new_image[start_y:end_y, start_x:end_x] = image[start_y:end_y, start_x:end_x] image[...] = new_image[...] \ No newline at end of file diff --git a/SerialPrograms/Scripts/image_viewer.py b/SerialPrograms/Scripts/image_viewer.py index ea2a2b2ba6..29c00bec6d 100755 --- a/SerialPrograms/Scripts/image_viewer.py +++ b/SerialPrograms/Scripts/image_viewer.py @@ -1,253 +1,253 @@ -#!python3 - -""" -A simple image viewer (using OpenCV) with basic ability to check pixel location and color. -It can also draw boxes on the image and print their locations. Useful for writing -PokemonAutomation visual inference methods. - -Single left clicking on a pixel shows you the info of that pixel. Note it may also print the -alpha channel value. - -- Press 'w', 's', 'a', 'd' to move the selected pixels around. - -To draw a box: left click and drag the rectangle. -You can draw multiple boxes on the screen. - -- Press 'i' to dump the information of those boxes so you can copy them into the code, - or into check_detector_regions.py to fine tune the boxes. - -- Press 'backspace/delete' to delete the current selected box. If no box is selected, - delete the last added box. Select an existing box by right clicking. - -- Press 'ESC' to exit the program. -""" - - -import cv2 -import numpy as np - -class ImageViewer: - def __init__(self, image, highlight_list = []): - self.image = image - self.hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) - self.selected_pixel = (-1, -1) - self.buffer = image.copy() - self.window_name = 'image' - self.height = image.shape[0] - self.width = image.shape[1] - self.highlight_list = highlight_list - self.cur_highlight_index = -1 - self.rects = [] # List of Tuple[int,int,int,int]: each tuple: start_x, start_y, end_x, end_y - self.cur_rect_index = -1 - self.mouse_down = False - self.mouse_move_counter = 0 - self.nc = image.shape[2] # num_channel - # The size of the cross used to highlight a selected pixel - self.cross_size = max(1, min(self.width, self.height) // 200) - - def _solid_color(self, color): - return color if self.image.shape[2] == 3 else color + [255] - - def _set_color_to_buffer(self, coord, color): - x, y = coord - if x >= 0 and x < self.width and y >= 0 and y < self.height: - self.buffer[y, x] = self._solid_color(color) - - def _render(self): - self.buffer = self.image.copy() - if self.selected_pixel[0] >= 0 and self.selected_pixel[1] >= 0: - p = self.selected_pixel - # Draw a red cross - self._set_color_to_buffer(p, color=[0, 0, 255]) - for i in range(1, self.cross_size): - self._set_color_to_buffer((p[0]-i, p[1]), color=[0, 0, 255]) - self._set_color_to_buffer((p[0]+i, p[1]), color=[0, 0, 255]) - self._set_color_to_buffer((p[0], p[1]-i), color=[0, 0, 255]) - self._set_color_to_buffer((p[0], p[1]+i), color=[0, 0, 255]) - - if self.cur_highlight_index >= 0 and len(self.highlight_list) > 0: - for pixel in self.highlight_list[self.cur_highlight_index]: - self.buffer[pixel[1], pixel[0]] = self._solid_color([255, 0, 0]) - - for i, rect in enumerate(self.rects): - # print(f"In render(): {rect}") - # rect: [start_x start_y end_x end_y] - width = 2 - color = (0, 0, 255) if i == self.cur_rect_index else (255, 0, 0) - self.buffer = cv2.rectangle(self.buffer, (rect[0], rect[1]), (rect[2], rect[3]), color, width) - - cv2.imshow(self.window_name, self.buffer) - # self.fullscreen = False - - def _change_selected_rect(self, x, y): - if len(self.rects) == 0: - return - - min_dist = 0 - for i, rect in enumerate(self.rects): - min_x = min(rect[0], rect[2]) - max_x = max(rect[0], rect[2]) - min_y = min(rect[1], rect[3]) - max_y = max(rect[1], rect[3]) - dist_x = min_x - x if x <= min_x else (x - max_x if x >= max_x else 0) - dist_y = min_y - y if y <= min_y else (y - max_y if y >= max_y else 0) - dist = dist_x ** 2 + dist_y ** 2 - if i == 0 or dist < min_dist: - min_dist = dist - self.cur_rect_index = i - print(f"Selected rect No.{self.cur_rect_index}/{len(self.rects)}: {rect}.") - - def _print_pixel(self, coord): - p = self.image[coord[1], coord[0]] - print(f"Pixel (x,y) = ({coord[0]}, {coord[1]}), ({coord[0]/self.width:0.3}, {coord[1]/self.height:0.3}), " - + f"rgb=[{str(p[3]) + ',' if self.nc == 4 else ''}{p[2]},{p[1]},{p[0]}] hsv={self.hsv_image[coord[1], coord[0]]}") - - def _print_rect(self, i, rect): - crop = self.image[rect[1]:rect[3], rect[0]:rect[2]].astype(float) / 255.0 - num_pixels = crop.shape[0] * crop.shape[1] - # crop_sum shape: (4), 4 is channel count - crop_sum = np.sum(crop, (0, 1)) - # crop_avg shape: (4), 4 is channel count - crop_avg = crop_sum / num_pixels - # crop_sqr_sum shape: (4), 4 is channel count - crop_sqr_sum = np.sum(np.square(crop), (0, 1)) - - crop_stddev = np.sqrt( np.clip(crop_sqr_sum - (np.square(crop_sum) / num_pixels), 0, None) / (num_pixels-1)) - - avg_sum = np.sum(crop_avg) - avg_ratio = [1/3., 1/3., 1/3.] if avg_sum == 0. else crop_avg / avg_sum - avg_ratio = np.array([avg_ratio[2], avg_ratio[1], avg_ratio[0]]) - x = rect[0] / self.width - y = rect[1] / self.height - w = (rect[2] - rect[0]) / self.width - h = (rect[3] - rect[1]) / self.height - - stddev_sum = np.sum(crop_stddev) - print(f"Rect No.{i}, ({x:.3f}, {y:.3f}, {w:.3f}, {h:.3f}) rgb ratio: {avg_ratio[0]:.3f}:{avg_ratio[1]:.3f}:{avg_ratio[2]:.3f}, stddev sum: {stddev_sum:.3g}") - - def _move_crop(self, o0, o1, o2, o3): - if self.cur_crop_index >= 0 and self.cur_crop_index < len(self.crops): - self.crops[self.cur_crop_index][0] += o0 - self.crops[self.cur_crop_index][1] += o1 - self.crops[self.cur_crop_index][2] += o2 - self.crops[self.cur_crop_index][3] += o3 - self._render() - - def _mouse_callback(self, event, x, y, flags, param): - redraw = False - - # Dragging selected_pixel rectangle - if event == cv2.EVENT_LBUTTONDOWN: - self.mouse_down = True - self.cur_rect_index = len(self.rects) - self.rects.append([x, y, x, y]) - self.mouse_move_counter = 0 - elif event == cv2.EVENT_LBUTTONUP: - self.mouse_down = False - assert len(self.rects) > 0 - if self.rects[-1][0] == self.rects[-1][2] and self.rects[-1][1] == self.rects[-1][3]: - self.cur_rect_index -= 1 - self.rects.pop() - self._change_selected_rect(x, y) - self._print_pixel((x, y)) - if self.selected_pixel != (x, y): - self.selected_pixel = (x, y) - redraw = True - else: - # sanitize the rect: - if self.cur_rect_index >= 0 and self.cur_rect_index < len(self.rects): - rect = self.rects[self.cur_rect_index] - min_x = min(rect[0], rect[2]) - max_x = max(rect[0], rect[2]) - min_y = min(rect[1], rect[3]) - max_y = max(rect[1], rect[3]) - rect[0] = min_x - rect[1] = min_y - rect[2] = max_x - rect[3] = max_y - redraw = True - mouse_move_counter = 0 - elif event == cv2.EVENT_RBUTTONUP: # right click to select existing rect to edit - if len(self.rects) > 0: - self._change_selected_rect(x, y) - redraw = True - elif event == cv2.EVENT_MOUSEMOVE and self.mouse_down: - if self.cur_rect_index >= 0 and self.cur_rect_index < len(self.rects): - old_loc = (self.rects[self.cur_rect_index][2], self.rects[self.cur_rect_index][3]) - if old_loc != (x, y): - self.rects[self.cur_rect_index][2] = x - self.rects[self.cur_rect_index][3] = y - # if mouse_move_counter % 2 == 0: - redraw = True - self.mouse_move_counter += 1 - - if redraw: - self._render() - - def run(self): - - cv2.imshow(self.window_name, self.buffer) - cv2.setWindowProperty(self.window_name, cv2.WND_PROP_TOPMOST, 1) - cv2.setMouseCallback(self.window_name, self._mouse_callback) - - while True: - key = cv2.waitKey(0) - - if key == 27: # esc - cv2.destroyAllWindows() - break - elif key == 119: # w - if self.selected_pixel[0] >= 0 and self.selected_pixel[1] >= 1: - self.selected_pixel = (self.selected_pixel[0], self.selected_pixel[1]-1) - self._print_pixel(self.selected_pixel) - elif key == 115: # s - if self.selected_pixel[0] >= 0 and self.selected_pixel[1] >= 0 and self.selected_pixel[1] + 1 < self.height: - self.selected_pixel = (self.selected_pixel[0], self.selected_pixel[1]+1) - self._print_pixel(self.selected_pixel) - elif key == 97: # a - if self.selected_pixel[0] >= 1 and self.selected_pixel[1] >= 0: - self.selected_pixel = (self.selected_pixel[0]-1, self.selected_pixel[1]) - self._print_pixel(self.selected_pixel) - elif key == 100: # d - if self.selected_pixel[0] >= 0 and self.selected_pixel[1] >= 0 and self.selected_pixel[0] + 1 < self.width: - self.selected_pixel = (self.selected_pixel[0]+1, self.selected_pixel[1]) - self._print_pixel(self.selected_pixel) - elif key == 44: # , - if len(self.highlight_list) > 0: - self.cur_highlight_index = len(self.highlight_list) - 1 if self.cur_highlight_index < 0 else self.cur_highlight_index-1 - print(f"Change highlight index to {self.cur_highlight_index}/{len(self.highlight_list)}") - elif key == 46: # . - if len(self.highlight_list) > 0: - self.cur_highlight_index = -1 if self.cur_highlight_index == len(self.highlight_list)-1 else self.cur_highlight_index+1 - print(f"Change highlight index to {self.cur_highlight_index}/{len(self.highlight_list)}") - elif key == 127 or key == 8: # DEL or backspace (BS), remove selected rectangle - mouse_down = False - mouse_move_counter = 0 - if len(self.rects) > 0: - if self.cur_rect_index < 0: - self.cur_rect_index = len(self.crops) - 1 - del self.rects[self.cur_rect_index] - if len(self.rects) == 0: - self.cur_rect_index = -1 - elif self.cur_rect_index >= len(self.rects): - self.cur_rect_index = len(self.rects) - 1 - elif key == 105: # i - for i, rect in enumerate(self.rects): - self._print_rect(i, rect) - else: - print(f"Pressed key {key}") - self._render() - -if __name__ == '__main__': - import sys - assert len(sys.argv) == 2 - - filename = sys.argv[1] - - image = cv2.imread(filename, cv2.IMREAD_UNCHANGED) - - height = image.shape[0] - width = image.shape[1] - print(f"Load image from {filename}, size: {width} x {height}") - viewer = ImageViewer(image) - viewer.run() +#!python3 + +""" +A simple image viewer (using OpenCV) with basic ability to check pixel location and color. +It can also draw boxes on the image and print their locations. Useful for writing +PokemonAutomation visual inference methods. + +Single left clicking on a pixel shows you the info of that pixel. Note it may also print the +alpha channel value. + +- Press 'w', 's', 'a', 'd' to move the selected pixels around. + +To draw a box: left click and drag the rectangle. +You can draw multiple boxes on the screen. + +- Press 'i' to dump the information of those boxes so you can copy them into the code, + or into check_detector_regions.py to fine tune the boxes. + +- Press 'backspace/delete' to delete the current selected box. If no box is selected, + delete the last added box. Select an existing box by right clicking. + +- Press 'ESC' to exit the program. +""" + + +import cv2 +import numpy as np + +class ImageViewer: + def __init__(self, image, highlight_list = []): + self.image = image + self.hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) + self.selected_pixel = (-1, -1) + self.buffer = image.copy() + self.window_name = 'image' + self.height = image.shape[0] + self.width = image.shape[1] + self.highlight_list = highlight_list + self.cur_highlight_index = -1 + self.rects = [] # List of Tuple[int,int,int,int]: each tuple: start_x, start_y, end_x, end_y + self.cur_rect_index = -1 + self.mouse_down = False + self.mouse_move_counter = 0 + self.nc = image.shape[2] # num_channel + # The size of the cross used to highlight a selected pixel + self.cross_size = max(1, min(self.width, self.height) // 200) + + def _solid_color(self, color): + return color if self.image.shape[2] == 3 else color + [255] + + def _set_color_to_buffer(self, coord, color): + x, y = coord + if x >= 0 and x < self.width and y >= 0 and y < self.height: + self.buffer[y, x] = self._solid_color(color) + + def _render(self): + self.buffer = self.image.copy() + if self.selected_pixel[0] >= 0 and self.selected_pixel[1] >= 0: + p = self.selected_pixel + # Draw a red cross + self._set_color_to_buffer(p, color=[0, 0, 255]) + for i in range(1, self.cross_size): + self._set_color_to_buffer((p[0]-i, p[1]), color=[0, 0, 255]) + self._set_color_to_buffer((p[0]+i, p[1]), color=[0, 0, 255]) + self._set_color_to_buffer((p[0], p[1]-i), color=[0, 0, 255]) + self._set_color_to_buffer((p[0], p[1]+i), color=[0, 0, 255]) + + if self.cur_highlight_index >= 0 and len(self.highlight_list) > 0: + for pixel in self.highlight_list[self.cur_highlight_index]: + self.buffer[pixel[1], pixel[0]] = self._solid_color([255, 0, 0]) + + for i, rect in enumerate(self.rects): + # print(f"In render(): {rect}") + # rect: [start_x start_y end_x end_y] + width = 2 + color = (0, 0, 255) if i == self.cur_rect_index else (255, 0, 0) + self.buffer = cv2.rectangle(self.buffer, (rect[0], rect[1]), (rect[2], rect[3]), color, width) + + cv2.imshow(self.window_name, self.buffer) + # self.fullscreen = False + + def _change_selected_rect(self, x, y): + if len(self.rects) == 0: + return + + min_dist = 0 + for i, rect in enumerate(self.rects): + min_x = min(rect[0], rect[2]) + max_x = max(rect[0], rect[2]) + min_y = min(rect[1], rect[3]) + max_y = max(rect[1], rect[3]) + dist_x = min_x - x if x <= min_x else (x - max_x if x >= max_x else 0) + dist_y = min_y - y if y <= min_y else (y - max_y if y >= max_y else 0) + dist = dist_x ** 2 + dist_y ** 2 + if i == 0 or dist < min_dist: + min_dist = dist + self.cur_rect_index = i + print(f"Selected rect No.{self.cur_rect_index}/{len(self.rects)}: {rect}.") + + def _print_pixel(self, coord): + p = self.image[coord[1], coord[0]] + print(f"Pixel (x,y) = ({coord[0]}, {coord[1]}), ({coord[0]/self.width:0.3}, {coord[1]/self.height:0.3}), " + + f"rgb=[{str(p[3]) + ',' if self.nc == 4 else ''}{p[2]},{p[1]},{p[0]}] hsv={self.hsv_image[coord[1], coord[0]]}") + + def _print_rect(self, i, rect): + crop = self.image[rect[1]:rect[3], rect[0]:rect[2]].astype(float) / 255.0 + num_pixels = crop.shape[0] * crop.shape[1] + # crop_sum shape: (4), 4 is channel count + crop_sum = np.sum(crop, (0, 1)) + # crop_avg shape: (4), 4 is channel count + crop_avg = crop_sum / num_pixels + # crop_sqr_sum shape: (4), 4 is channel count + crop_sqr_sum = np.sum(np.square(crop), (0, 1)) + + crop_stddev = np.sqrt( np.clip(crop_sqr_sum - (np.square(crop_sum) / num_pixels), 0, None) / (num_pixels-1)) + + avg_sum = np.sum(crop_avg) + avg_ratio = [1/3., 1/3., 1/3.] if avg_sum == 0. else crop_avg / avg_sum + avg_ratio = np.array([avg_ratio[2], avg_ratio[1], avg_ratio[0]]) + x = rect[0] / self.width + y = rect[1] / self.height + w = (rect[2] - rect[0]) / self.width + h = (rect[3] - rect[1]) / self.height + + stddev_sum = np.sum(crop_stddev) + print(f"Rect No.{i}, ({x:.3f}, {y:.3f}, {w:.3f}, {h:.3f}) rgb ratio: {avg_ratio[0]:.3f}:{avg_ratio[1]:.3f}:{avg_ratio[2]:.3f}, stddev sum: {stddev_sum:.3g}") + + def _move_crop(self, o0, o1, o2, o3): + if self.cur_crop_index >= 0 and self.cur_crop_index < len(self.crops): + self.crops[self.cur_crop_index][0] += o0 + self.crops[self.cur_crop_index][1] += o1 + self.crops[self.cur_crop_index][2] += o2 + self.crops[self.cur_crop_index][3] += o3 + self._render() + + def _mouse_callback(self, event, x, y, flags, param): + redraw = False + + # Dragging selected_pixel rectangle + if event == cv2.EVENT_LBUTTONDOWN: + self.mouse_down = True + self.cur_rect_index = len(self.rects) + self.rects.append([x, y, x, y]) + self.mouse_move_counter = 0 + elif event == cv2.EVENT_LBUTTONUP: + self.mouse_down = False + assert len(self.rects) > 0 + if self.rects[-1][0] == self.rects[-1][2] and self.rects[-1][1] == self.rects[-1][3]: + self.cur_rect_index -= 1 + self.rects.pop() + self._change_selected_rect(x, y) + self._print_pixel((x, y)) + if self.selected_pixel != (x, y): + self.selected_pixel = (x, y) + redraw = True + else: + # sanitize the rect: + if self.cur_rect_index >= 0 and self.cur_rect_index < len(self.rects): + rect = self.rects[self.cur_rect_index] + min_x = min(rect[0], rect[2]) + max_x = max(rect[0], rect[2]) + min_y = min(rect[1], rect[3]) + max_y = max(rect[1], rect[3]) + rect[0] = min_x + rect[1] = min_y + rect[2] = max_x + rect[3] = max_y + redraw = True + mouse_move_counter = 0 + elif event == cv2.EVENT_RBUTTONUP: # right click to select existing rect to edit + if len(self.rects) > 0: + self._change_selected_rect(x, y) + redraw = True + elif event == cv2.EVENT_MOUSEMOVE and self.mouse_down: + if self.cur_rect_index >= 0 and self.cur_rect_index < len(self.rects): + old_loc = (self.rects[self.cur_rect_index][2], self.rects[self.cur_rect_index][3]) + if old_loc != (x, y): + self.rects[self.cur_rect_index][2] = x + self.rects[self.cur_rect_index][3] = y + # if mouse_move_counter % 2 == 0: + redraw = True + self.mouse_move_counter += 1 + + if redraw: + self._render() + + def run(self): + + cv2.imshow(self.window_name, self.buffer) + cv2.setWindowProperty(self.window_name, cv2.WND_PROP_TOPMOST, 1) + cv2.setMouseCallback(self.window_name, self._mouse_callback) + + while True: + key = cv2.waitKey(0) + + if key == 27: # esc + cv2.destroyAllWindows() + break + elif key == 119: # w + if self.selected_pixel[0] >= 0 and self.selected_pixel[1] >= 1: + self.selected_pixel = (self.selected_pixel[0], self.selected_pixel[1]-1) + self._print_pixel(self.selected_pixel) + elif key == 115: # s + if self.selected_pixel[0] >= 0 and self.selected_pixel[1] >= 0 and self.selected_pixel[1] + 1 < self.height: + self.selected_pixel = (self.selected_pixel[0], self.selected_pixel[1]+1) + self._print_pixel(self.selected_pixel) + elif key == 97: # a + if self.selected_pixel[0] >= 1 and self.selected_pixel[1] >= 0: + self.selected_pixel = (self.selected_pixel[0]-1, self.selected_pixel[1]) + self._print_pixel(self.selected_pixel) + elif key == 100: # d + if self.selected_pixel[0] >= 0 and self.selected_pixel[1] >= 0 and self.selected_pixel[0] + 1 < self.width: + self.selected_pixel = (self.selected_pixel[0]+1, self.selected_pixel[1]) + self._print_pixel(self.selected_pixel) + elif key == 44: # , + if len(self.highlight_list) > 0: + self.cur_highlight_index = len(self.highlight_list) - 1 if self.cur_highlight_index < 0 else self.cur_highlight_index-1 + print(f"Change highlight index to {self.cur_highlight_index}/{len(self.highlight_list)}") + elif key == 46: # . + if len(self.highlight_list) > 0: + self.cur_highlight_index = -1 if self.cur_highlight_index == len(self.highlight_list)-1 else self.cur_highlight_index+1 + print(f"Change highlight index to {self.cur_highlight_index}/{len(self.highlight_list)}") + elif key == 127 or key == 8: # DEL or backspace (BS), remove selected rectangle + mouse_down = False + mouse_move_counter = 0 + if len(self.rects) > 0: + if self.cur_rect_index < 0: + self.cur_rect_index = len(self.crops) - 1 + del self.rects[self.cur_rect_index] + if len(self.rects) == 0: + self.cur_rect_index = -1 + elif self.cur_rect_index >= len(self.rects): + self.cur_rect_index = len(self.rects) - 1 + elif key == 105: # i + for i, rect in enumerate(self.rects): + self._print_rect(i, rect) + else: + print(f"Pressed key {key}") + self._render() + +if __name__ == '__main__': + import sys + assert len(sys.argv) == 2 + + filename = sys.argv[1] + + image = cv2.imread(filename, cv2.IMREAD_UNCHANGED) + + height = image.shape[0] + width = image.shape[1] + print(f"Load image from {filename}, size: {width} x {height}") + viewer = ImageViewer(image) + viewer.run() diff --git a/SerialPrograms/Scripts/invert_json.py b/SerialPrograms/Scripts/invert_json.py index 3308959ec6..ce89f6b425 100644 --- a/SerialPrograms/Scripts/invert_json.py +++ b/SerialPrograms/Scripts/invert_json.py @@ -1,25 +1,25 @@ -import json -import sys - - -def main(): - if len(sys.argv) != 3: - print("usage: {} ".format(sys.argv[0])) - sys.exit(1) - - with open(sys.argv[1], "r", encoding="utf-8") as f: - data = json.load(f) - - inverted = {} - for key, value in data.items(): - for lang, word in value.items(): - if lang not in inverted: - inverted[lang] = {} - inverted[lang][key] = word - - with open(sys.argv[2], "w", encoding="utf-8") as f: - json.dump(inverted, f, indent=4, ensure_ascii=False) - - -if __name__ == "__main__": +import json +import sys + + +def main(): + if len(sys.argv) != 3: + print("usage: {} ".format(sys.argv[0])) + sys.exit(1) + + with open(sys.argv[1], "r", encoding="utf-8") as f: + data = json.load(f) + + inverted = {} + for key, value in data.items(): + for lang, word in value.items(): + if lang not in inverted: + inverted[lang] = {} + inverted[lang][key] = word + + with open(sys.argv[2], "w", encoding="utf-8") as f: + json.dump(inverted, f, indent=4, ensure_ascii=False) + + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/SerialPrograms/Scripts/make_ocr_json.py b/SerialPrograms/Scripts/make_ocr_json.py index 1a35905fca..988d23ce30 100644 --- a/SerialPrograms/Scripts/make_ocr_json.py +++ b/SerialPrograms/Scripts/make_ocr_json.py @@ -1,154 +1,154 @@ -""" -Generates an OCR json file for dictionary matching - -Set-up: -- Create a file named "target_words.txt". Put all your target dictionary words in this file, -with a new line between each phrase. Put this file in the same folder as this script. -- Adjust the start_line and end_line variables, which are hard-coded. -This helps to limit the script's search of the text dump files. -This helps with dealing with cases where the same English word is used by different parts of the game, but in a different -language, different words may be used. e.g. "Close" in English can be "Nah" or "Schließen" in German. -- Put all the text dump files for all languages in the same folder as this script. You can get the text dump files -from data miners such as https://x.com/mattyoukhana_ - - Ensure the names of the text dump files match the file names in the function `get_text_dump_filename_from_language()` - -How it works: -- each target word from "target_words.txt" is converted to a slug. -i.e. spaces are converted to dashes and converted to lowercase -- for each target word, "English.txt" is searched to find the location (line number and column) of the word. All the -text dump files for the various languages are formatted similarly. So, the above location will also point us to -the corresponding target word in other languages. -- the json OCR file is then created based on the slugs and the location of each target word for each of the languages -""" - -from typing import TextIO - - -start_line, end_line = 39560, 39665 # for menu option items -# start_line, end_line = 50255, 51178 # for all moves - -def find_location_from_target_word(text_dump_filename: str, target_word: str, start: int, end: int) -> tuple[int, int]: - with open(text_dump_filename, 'r', encoding='utf-8', errors='ignore') as file: - for row, line in enumerate(file): - if row < start: - continue - if row > end: - break - split_line = line.split("\t") - for column, cell in enumerate(split_line): - if cell.strip() == target_word: - return row, column - raise Exception("unable to find target word: " + target_word) - -def find_target_word_from_location(text_dump_filename: str, location: tuple[int, int]) -> str: - target_row = location[0] - target_column = location[1] - if target_row < 0: - raise IndexError("target_row index out of range") - with open(text_dump_filename, 'r', encoding='utf-8', errors='ignore') as file: - for row, line in enumerate(file): - if row == target_row: - split_line = line.split("\t") - if target_column < 0 or target_column >= len(split_line): - raise IndexError("target_column index out of range") - for column, cell in enumerate(split_line): - if column == target_column: - return cell.strip() - raise IndexError("target_row index out of range") - - - -def get_slug_from_target_word(target_word: str) -> str: - slug = (target_word.replace(" ", "-") - .replace("'", "") - .replace("’", "") - .replace("é","e") - .casefold()) - return slug - -def generate_ocr_json_file(target_words_filename: str, start: int, end: int): - list_of_slug_location_pair = get_list_of_slug_location_pair(target_words_filename, "English.txt", start, end) - json_output_filename = "OCR.json" - with open(json_output_filename, 'a', encoding='utf-8') as file: - file.truncate(0) # clear file contents first - file.write("{\n") - generate_ocr_json_file_one_language(file, "deu", list_of_slug_location_pair) - file.write(",\n") - generate_ocr_json_file_one_language(file, "eng", list_of_slug_location_pair) - file.write(",\n") - generate_ocr_json_file_one_language(file, "spa", list_of_slug_location_pair) - file.write(",\n") - generate_ocr_json_file_one_language(file, "fra", list_of_slug_location_pair) - file.write(",\n") - generate_ocr_json_file_one_language(file, "ita", list_of_slug_location_pair) - file.write(",\n") - generate_ocr_json_file_one_language(file, "kor", list_of_slug_location_pair) - file.write(",\n") - generate_ocr_json_file_one_language(file, "jpn", list_of_slug_location_pair) - file.write(",\n") - generate_ocr_json_file_one_language(file, "chi_sim", list_of_slug_location_pair) - file.write(",\n") - generate_ocr_json_file_one_language(file, "chi_tra", list_of_slug_location_pair) - file.write("\n}") - - -# start and end parameter so we only search certain portions of file. -# This helps with dealing with cases where the same English word is used by different parts of the game, -# but in a different language, different words may be used. -# e.g. "Close" in English can be "Nah" or "Schließen" in German. -def get_list_of_slug_location_pair(target_words_filename: str, text_dump_filename: str, start: int, end: int) -> list[tuple[str, tuple[int, int]]]: - with open(target_words_filename, 'r', encoding='utf-8', errors='ignore') as file: - list_of_slug_location_pair = [] - for line in file: - target_word = line.strip() - slug = get_slug_from_target_word(target_word) - location = find_location_from_target_word(text_dump_filename, target_word, start, end) - slug_location_pair = (slug, location) - list_of_slug_location_pair.append(slug_location_pair) - print("Done finding list_of_slug_location_pair") - return list_of_slug_location_pair - - -def generate_ocr_json_file_one_language(file: TextIO, language: str, list_of_slug_location_pair: list[tuple[str, tuple[int, int]]]): - text_dump_filename = get_text_dump_filename_from_language(language) - file.write("\t" + "\"" + language + "\"" + ": " + "{" + "\n") - for i, slug_location_pair in enumerate(list_of_slug_location_pair): - slug = slug_location_pair[0] - location = slug_location_pair[1] - target_word = find_target_word_from_location(text_dump_filename, location) - if i != 0: - file.write(",\n") - file.write("\t\t" + "\"" + slug + "\"" + ": " + "[ " + "\"" + target_word + "\"" + " ]") - file.write("\n\t}") - print("Done language: " + language) - -def get_text_dump_filename_from_language(language: str) -> str: - text_dump_filename = "" - match language: - case "deu": - text_dump_filename = "Deutsch.txt" - case "eng": - text_dump_filename = "English.txt" - case "spa": - text_dump_filename = "Español (España).txt" - case "fra": - text_dump_filename = "Français.txt" - case "ita": - text_dump_filename = "Italiano.txt" - case "kor": - text_dump_filename = "한국어.txt" - case "jpn": - text_dump_filename = "日本語 (カタカナ).txt" - case "chi_sim": - text_dump_filename = "简体中文.txt" - case "chi_tra": - text_dump_filename = "繁體中文.txt" - - if text_dump_filename == "": - raise Exception("Language not recognized") - return text_dump_filename - - -generate_ocr_json_file("target_words.txt", start_line, end_line) - - +""" +Generates an OCR json file for dictionary matching + +Set-up: +- Create a file named "target_words.txt". Put all your target dictionary words in this file, +with a new line between each phrase. Put this file in the same folder as this script. +- Adjust the start_line and end_line variables, which are hard-coded. +This helps to limit the script's search of the text dump files. +This helps with dealing with cases where the same English word is used by different parts of the game, but in a different +language, different words may be used. e.g. "Close" in English can be "Nah" or "Schließen" in German. +- Put all the text dump files for all languages in the same folder as this script. You can get the text dump files +from data miners such as https://x.com/mattyoukhana_ + - Ensure the names of the text dump files match the file names in the function `get_text_dump_filename_from_language()` + +How it works: +- each target word from "target_words.txt" is converted to a slug. +i.e. spaces are converted to dashes and converted to lowercase +- for each target word, "English.txt" is searched to find the location (line number and column) of the word. All the +text dump files for the various languages are formatted similarly. So, the above location will also point us to +the corresponding target word in other languages. +- the json OCR file is then created based on the slugs and the location of each target word for each of the languages +""" + +from typing import TextIO + + +start_line, end_line = 39560, 39665 # for menu option items +# start_line, end_line = 50255, 51178 # for all moves + +def find_location_from_target_word(text_dump_filename: str, target_word: str, start: int, end: int) -> tuple[int, int]: + with open(text_dump_filename, 'r', encoding='utf-8', errors='ignore') as file: + for row, line in enumerate(file): + if row < start: + continue + if row > end: + break + split_line = line.split("\t") + for column, cell in enumerate(split_line): + if cell.strip() == target_word: + return row, column + raise Exception("unable to find target word: " + target_word) + +def find_target_word_from_location(text_dump_filename: str, location: tuple[int, int]) -> str: + target_row = location[0] + target_column = location[1] + if target_row < 0: + raise IndexError("target_row index out of range") + with open(text_dump_filename, 'r', encoding='utf-8', errors='ignore') as file: + for row, line in enumerate(file): + if row == target_row: + split_line = line.split("\t") + if target_column < 0 or target_column >= len(split_line): + raise IndexError("target_column index out of range") + for column, cell in enumerate(split_line): + if column == target_column: + return cell.strip() + raise IndexError("target_row index out of range") + + + +def get_slug_from_target_word(target_word: str) -> str: + slug = (target_word.replace(" ", "-") + .replace("'", "") + .replace("’", "") + .replace("é","e") + .casefold()) + return slug + +def generate_ocr_json_file(target_words_filename: str, start: int, end: int): + list_of_slug_location_pair = get_list_of_slug_location_pair(target_words_filename, "English.txt", start, end) + json_output_filename = "OCR.json" + with open(json_output_filename, 'a', encoding='utf-8') as file: + file.truncate(0) # clear file contents first + file.write("{\n") + generate_ocr_json_file_one_language(file, "deu", list_of_slug_location_pair) + file.write(",\n") + generate_ocr_json_file_one_language(file, "eng", list_of_slug_location_pair) + file.write(",\n") + generate_ocr_json_file_one_language(file, "spa", list_of_slug_location_pair) + file.write(",\n") + generate_ocr_json_file_one_language(file, "fra", list_of_slug_location_pair) + file.write(",\n") + generate_ocr_json_file_one_language(file, "ita", list_of_slug_location_pair) + file.write(",\n") + generate_ocr_json_file_one_language(file, "kor", list_of_slug_location_pair) + file.write(",\n") + generate_ocr_json_file_one_language(file, "jpn", list_of_slug_location_pair) + file.write(",\n") + generate_ocr_json_file_one_language(file, "chi_sim", list_of_slug_location_pair) + file.write(",\n") + generate_ocr_json_file_one_language(file, "chi_tra", list_of_slug_location_pair) + file.write("\n}") + + +# start and end parameter so we only search certain portions of file. +# This helps with dealing with cases where the same English word is used by different parts of the game, +# but in a different language, different words may be used. +# e.g. "Close" in English can be "Nah" or "Schließen" in German. +def get_list_of_slug_location_pair(target_words_filename: str, text_dump_filename: str, start: int, end: int) -> list[tuple[str, tuple[int, int]]]: + with open(target_words_filename, 'r', encoding='utf-8', errors='ignore') as file: + list_of_slug_location_pair = [] + for line in file: + target_word = line.strip() + slug = get_slug_from_target_word(target_word) + location = find_location_from_target_word(text_dump_filename, target_word, start, end) + slug_location_pair = (slug, location) + list_of_slug_location_pair.append(slug_location_pair) + print("Done finding list_of_slug_location_pair") + return list_of_slug_location_pair + + +def generate_ocr_json_file_one_language(file: TextIO, language: str, list_of_slug_location_pair: list[tuple[str, tuple[int, int]]]): + text_dump_filename = get_text_dump_filename_from_language(language) + file.write("\t" + "\"" + language + "\"" + ": " + "{" + "\n") + for i, slug_location_pair in enumerate(list_of_slug_location_pair): + slug = slug_location_pair[0] + location = slug_location_pair[1] + target_word = find_target_word_from_location(text_dump_filename, location) + if i != 0: + file.write(",\n") + file.write("\t\t" + "\"" + slug + "\"" + ": " + "[ " + "\"" + target_word + "\"" + " ]") + file.write("\n\t}") + print("Done language: " + language) + +def get_text_dump_filename_from_language(language: str) -> str: + text_dump_filename = "" + match language: + case "deu": + text_dump_filename = "Deutsch.txt" + case "eng": + text_dump_filename = "English.txt" + case "spa": + text_dump_filename = "Español (España).txt" + case "fra": + text_dump_filename = "Français.txt" + case "ita": + text_dump_filename = "Italiano.txt" + case "kor": + text_dump_filename = "한국어.txt" + case "jpn": + text_dump_filename = "日本語 (カタカナ).txt" + case "chi_sim": + text_dump_filename = "简体中文.txt" + case "chi_tra": + text_dump_filename = "繁體中文.txt" + + if text_dump_filename == "": + raise Exception("Language not recognized") + return text_dump_filename + + +generate_ocr_json_file("target_words.txt", start_line, end_line) + + diff --git a/SerialPrograms/Scripts/prune_json.py b/SerialPrograms/Scripts/prune_json.py index ec51a7656a..c6932f71ce 100644 --- a/SerialPrograms/Scripts/prune_json.py +++ b/SerialPrograms/Scripts/prune_json.py @@ -1,125 +1,125 @@ -import json - -filling_slugs = [ - "apple", - "avocado", - "bacon", - "baguette", - "banana", - "basil", - "cheese", - "cherry-tomatoes", - "chorizo", - "cucumber", - "egg", - "fried-fillet", - "green-bell-pepper", - "ham", - "hamburger", - "herbed-sausage", - "jalapeño", - "kiwi", - "klawf-stick", - "lettuce", - "noodles", - "onion", - "pickle", - "pineapple", - "potato-salad", - "potato-tortilla", - "prosciutto", - "red-bell-pepper", - "red-onion", - "rice", - "smoked-fillet", - "strawberry", - "tofu", - "tomato", - "watercress", - "yellow-bell-pepper"] - -condiment_slugs = [ - "butter", - "chili-sauce", - "cream-cheese", - "curry-powder", - "horseradish", - "jam", - "ketchup", - "marmalade", - "mayonnaise", - "mustard", - "olive-oil", - "peanut-butter", - "pepper", - "salt", - "vinegar", - "wasabi", - "whipped-cream", - "yogurt", - "bitter-herba-mystica", - "spicy-herba-mystica", - "salty-herba-mystica", - "sour-herba-mystica", - "sweet-herba-mystica"] - -pick_slugs = [ - "blue-flag-pick", - "blue-poke-ball-pick", - "blue-sky-flower-pick", - "gold-pick", - "green-poke-ball-pick", - "heroic-sword-pick", - "magical-heart-pick", - "magical-star-pick", - "parasol-pick", - "party-sparkler-pick", - "pika-pika-pick", - "red-flag-pick", - "red-poke-ball-pick", - "silver-pick", - "smiling-vee-pick", - "sunrise-flower-pick", - "sunset-flower-pick", - "vee-vee-pick", - "winking-pika-pick"] - -def filter_keys(obj, keys): - if isinstance(obj, dict): - return {k: filter_keys(v, keys) for k, v in obj.items() if k not in keys} - elif isinstance(obj, list): - return [filter_keys(elem, keys) for elem in obj] - else: - return obj - -with open("OCR.json", "r", encoding="utf-8") as f: - input_ocr = json.load(f) - -output_ocr_condiments = filter_keys(input_ocr, filling_slugs + pick_slugs) -with open('SandwichCondimentOCR.json', 'w', encoding="utf-8") as f: - json.dump(output_ocr_condiments, f, indent=4, ensure_ascii=False) - -output_ocr_fillings = filter_keys(input_ocr, condiment_slugs + pick_slugs) -with open('SandwichFillingOCR.json', 'w', encoding="utf-8") as f: - json.dump(output_ocr_fillings, f, indent=4, ensure_ascii=False) - -output_ocr_picks = filter_keys(input_ocr, condiment_slugs + filling_slugs) -with open('SandwichPickOCR.json', 'w', encoding="utf-8") as f: - json.dump(output_ocr_picks, f, indent=4, ensure_ascii=False) - - - -with open("Sprites.json", "r", encoding="utf-8") as f: - input_sprite = json.load(f) - -output_sprite_condiments = filter_keys(input_sprite, filling_slugs + pick_slugs) -with open('SandwichCondimentSprites.json', 'w', encoding="utf-8") as f: - json.dump(output_sprite_condiments, f, indent=4, ensure_ascii=False) - -output_sprite_fillings = filter_keys(input_sprite, condiment_slugs + pick_slugs) -with open('SandwichFillingSprites.json', 'w', encoding="utf-8") as f: - json.dump(output_sprite_fillings, f, indent=4, ensure_ascii=False) - -output_sprite_picks = filter_keys(input_sprite, condiment_slugs + filling_slugs) -with open('SandwichPickSprites.json', 'w', encoding="utf-8") as f: +import json + +filling_slugs = [ + "apple", + "avocado", + "bacon", + "baguette", + "banana", + "basil", + "cheese", + "cherry-tomatoes", + "chorizo", + "cucumber", + "egg", + "fried-fillet", + "green-bell-pepper", + "ham", + "hamburger", + "herbed-sausage", + "jalapeño", + "kiwi", + "klawf-stick", + "lettuce", + "noodles", + "onion", + "pickle", + "pineapple", + "potato-salad", + "potato-tortilla", + "prosciutto", + "red-bell-pepper", + "red-onion", + "rice", + "smoked-fillet", + "strawberry", + "tofu", + "tomato", + "watercress", + "yellow-bell-pepper"] + +condiment_slugs = [ + "butter", + "chili-sauce", + "cream-cheese", + "curry-powder", + "horseradish", + "jam", + "ketchup", + "marmalade", + "mayonnaise", + "mustard", + "olive-oil", + "peanut-butter", + "pepper", + "salt", + "vinegar", + "wasabi", + "whipped-cream", + "yogurt", + "bitter-herba-mystica", + "spicy-herba-mystica", + "salty-herba-mystica", + "sour-herba-mystica", + "sweet-herba-mystica"] + +pick_slugs = [ + "blue-flag-pick", + "blue-poke-ball-pick", + "blue-sky-flower-pick", + "gold-pick", + "green-poke-ball-pick", + "heroic-sword-pick", + "magical-heart-pick", + "magical-star-pick", + "parasol-pick", + "party-sparkler-pick", + "pika-pika-pick", + "red-flag-pick", + "red-poke-ball-pick", + "silver-pick", + "smiling-vee-pick", + "sunrise-flower-pick", + "sunset-flower-pick", + "vee-vee-pick", + "winking-pika-pick"] + +def filter_keys(obj, keys): + if isinstance(obj, dict): + return {k: filter_keys(v, keys) for k, v in obj.items() if k not in keys} + elif isinstance(obj, list): + return [filter_keys(elem, keys) for elem in obj] + else: + return obj + +with open("OCR.json", "r", encoding="utf-8") as f: + input_ocr = json.load(f) + +output_ocr_condiments = filter_keys(input_ocr, filling_slugs + pick_slugs) +with open('SandwichCondimentOCR.json', 'w', encoding="utf-8") as f: + json.dump(output_ocr_condiments, f, indent=4, ensure_ascii=False) + +output_ocr_fillings = filter_keys(input_ocr, condiment_slugs + pick_slugs) +with open('SandwichFillingOCR.json', 'w', encoding="utf-8") as f: + json.dump(output_ocr_fillings, f, indent=4, ensure_ascii=False) + +output_ocr_picks = filter_keys(input_ocr, condiment_slugs + filling_slugs) +with open('SandwichPickOCR.json', 'w', encoding="utf-8") as f: + json.dump(output_ocr_picks, f, indent=4, ensure_ascii=False) + + + +with open("Sprites.json", "r", encoding="utf-8") as f: + input_sprite = json.load(f) + +output_sprite_condiments = filter_keys(input_sprite, filling_slugs + pick_slugs) +with open('SandwichCondimentSprites.json', 'w', encoding="utf-8") as f: + json.dump(output_sprite_condiments, f, indent=4, ensure_ascii=False) + +output_sprite_fillings = filter_keys(input_sprite, condiment_slugs + pick_slugs) +with open('SandwichFillingSprites.json', 'w', encoding="utf-8") as f: + json.dump(output_sprite_fillings, f, indent=4, ensure_ascii=False) + +output_sprite_picks = filter_keys(input_sprite, condiment_slugs + filling_slugs) +with open('SandwichPickSprites.json', 'w', encoding="utf-8") as f: json.dump(output_sprite_picks, f, indent=4, ensure_ascii=False) \ No newline at end of file