diff --git a/.cursor/rules/coding-standards.mdc b/.cursor/rules/coding-standards.mdc new file mode 100644 index 0000000..ebc538b --- /dev/null +++ b/.cursor/rules/coding-standards.mdc @@ -0,0 +1,72 @@ +--- +description: Project-wide coding standards — English-only docs/comments, Python naming, reuse, and clean code +alwaysApply: true +--- + +# Coding Standards + +All prompts and changes in this project must follow these rules. + +--- + +## Language + +- **Comments and documentation must be in English** (including docstrings, README, DOCS.md, and inline comments). +- User-facing strings (logs, UI) may stay in the project’s existing language if that is the current convention. + +--- + +## Python Naming Conventions + +Follow PEP 8; use these consistently: + +| Kind | Convention | Example | +|------|------------|--------| +| **Modules / files** | `snake_case` | `bot_engine.py`, `window_capture.py` | +| **Functions / methods** | `snake_case` | `get_screenshot()`, `find_template()` | +| **Variables / parameters** | `snake_case` | `screenshot`, `threshold`, `max_tries` | +| **Constants** | `UPPER_SNAKE_CASE` | `DEFAULT_THRESHOLD`, `MAX_RETRIES` | +| **Classes** | `PascalCase` | `FunctionRunner`, `Vision` | +| **Private** (module/class internal) | leading `_` | `_advance_step()`, `_roi_bounds` | +| **“Protected” / internal** | leading `_` | `_base_zoomout_search_img()` | + +- Avoid single-letter names except for trivial loop indices (e.g. `i`, `j`). +- Prefer descriptive names; avoid abbreviations unless very common (e.g. `img`, `rect`, `cfg`). + +--- + +## Before Adding New Code + +- **Check if equivalent logic already exists** (same module, other modules, or helpers). Reuse or extend it instead of duplicating. +- **Search the codebase** for similar behavior (e.g. “find template”, “click at position”) before implementing from scratch. +- Prefer **small, focused functions** that can be reused; avoid long inline blocks that do multiple unrelated things. + +--- + +## Clean & Reusable Code + +- **DRY**: Do not repeat yourself; extract shared logic into functions or helpers. +- **Single responsibility**: Each function/class should do one clear thing. +- **Reusability**: Prefer parameters and config over hardcoded values so behavior can be reused. +- **Readability**: Prefer clear control flow and named variables over clever one-liners. +- **No premature optimization**: Write simple, correct code first; optimize only when needed and with evidence. + +--- + +## After Editing Python (agents) + +- **Always verify Python syntax** after finishing work on any `.py` files—do not treat the task as complete until syntax checks pass. +- Run, for example: + - `python -m py_compile path/to/changed_file.py` for each modified file, or + - `python -m compileall -q path/to/touched_package` when many files under one directory changed. +- Fix any `SyntaxError` or compile failures before concluding the task. + +--- + +## Additional Rules + +- **Type hints**: Use type hints for function parameters and return values when they clarify intent or public APIs; not required for every trivial helper. +- **Docstrings**: Add docstrings (in English) for public functions, classes, and non-obvious behavior. +- **Error handling**: Prefer explicit handling and logging over bare `except`; avoid silencing errors without a clear reason. +- **Imports**: Group and order imports (stdlib → third-party → local); avoid unused imports. +- **Formatting**: Keep line length reasonable (e.g. ≤120 chars); break long lines for readability. diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..5d1b116 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,4 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) +kha_lastz_auto/__pycache__/ +kha_lastz_auto/.venv312 +kha_lastz_auto/playwright_zalo_data/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index b4e954c..b0618d0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,13 @@ __pycache__ /pydirectinput.egg-info /build -/dist \ No newline at end of file +/dist +**/logs +**/.env +**/.env_config +kha_lastz_auto/.env_config +kha_lastz_auto/playwright_zalo_data/ +kha_lastz_auto/debug/ +kha_lastz_auto/debug_ocr/ +kha_lastz_auto/debug_close_ui/ +kha_lastz_auto/debug_exclude/ \ No newline at end of file diff --git a/001_intro/albion_cabbage.jpg b/001_intro/albion_cabbage.jpg deleted file mode 100644 index 2c50ef1..0000000 Binary files a/001_intro/albion_cabbage.jpg and /dev/null differ diff --git a/001_intro/albion_farm.jpg b/001_intro/albion_farm.jpg deleted file mode 100644 index bcd1e45..0000000 Binary files a/001_intro/albion_farm.jpg and /dev/null differ diff --git a/001_intro/main.py b/001_intro/main.py deleted file mode 100644 index 4482d39..0000000 --- a/001_intro/main.py +++ /dev/null @@ -1,69 +0,0 @@ -import cv2 as cv -import numpy as np -import os - - -# Change the working directory to the folder this script is in. -# Doing this because I'll be putting the files from each video in their own folder on GitHub -os.chdir(os.path.dirname(os.path.abspath(__file__))) - -# Can use IMREAD flags to do different pre-processing of image files, -# like making them grayscale or reducing the size. -# https://docs.opencv.org/4.2.0/d4/da8/group__imgcodecs.html -haystack_img = cv.imread('albion_farm.jpg', cv.IMREAD_UNCHANGED) -needle_img = cv.imread('albion_cabbage.jpg', cv.IMREAD_UNCHANGED) - -# There are 6 comparison methods to choose from: -# TM_CCOEFF, TM_CCOEFF_NORMED, TM_CCORR, TM_CCORR_NORMED, TM_SQDIFF, TM_SQDIFF_NORMED -# You can see the differences at a glance here: -# https://docs.opencv.org/master/d4/dc6/tutorial_py_template_matching.html -# Note that the values are inverted for TM_SQDIFF and TM_SQDIFF_NORMED -result = cv.matchTemplate(haystack_img, needle_img, cv.TM_CCOEFF_NORMED) - -# You can view the result of matchTemplate() like this: -#cv.imshow('Result', result) -#cv.waitKey() -# If you want to save this result to a file, you'll need to normalize the result array -# from 0..1 to 0..255, see: -# https://stackoverflow.com/questions/35719480/opencv-black-image-after-matchtemplate -#cv.imwrite('result_CCOEFF_NORMED.jpg', result * 255) - -# Get the best match position from the match result. -min_val, max_val, min_loc, max_loc = cv.minMaxLoc(result) -# The max location will contain the upper left corner pixel position for the area -# that most closely matches our needle image. The max value gives an indication -# of how similar that find is to the original needle, where 1 is perfect and -1 -# is exact opposite. -print('Best match top left position: %s' % str(max_loc)) -print('Best match confidence: %s' % max_val) - -# If the best match value is greater than 0.8, we'll trust that we found a match -threshold = 0.8 -if max_val >= threshold: - print('Found needle.') - - # Get the size of the needle image. With OpenCV images, you can get the dimensions - # via the shape property. It returns a tuple of the number of rows, columns, and - # channels (if the image is color): - needle_w = needle_img.shape[1] - needle_h = needle_img.shape[0] - - # Calculate the bottom right corner of the rectangle to draw - top_left = max_loc - bottom_right = (top_left[0] + needle_w, top_left[1] + needle_h) - - # Draw a rectangle on our screenshot to highlight where we found the needle. - # The line color can be set as an RGB tuple - cv.rectangle(haystack_img, top_left, bottom_right, - color=(0, 255, 0), thickness=2, lineType=cv.LINE_4) - - # You can view the processed screenshot like this: - #cv.imshow('Result', haystack_img) - #cv.waitKey() - # Or you can save the results to a file. - # imwrite() will smartly format our output image based on the extension we give it - # https://docs.opencv.org/3.4/d4/da8/group__imgcodecs.html#gabbc7ef1aa2edfaa87772f1202d67e0ce - cv.imwrite('result.jpg', haystack_img) - -else: - print('Needle not found.') diff --git a/001_intro/result.jpg b/001_intro/result.jpg deleted file mode 100644 index c7d618d..0000000 Binary files a/001_intro/result.jpg and /dev/null differ diff --git a/001_intro/result_CCOEFF_NORMED.jpg b/001_intro/result_CCOEFF_NORMED.jpg deleted file mode 100644 index 76a7fa3..0000000 Binary files a/001_intro/result_CCOEFF_NORMED.jpg and /dev/null differ diff --git a/002_match_multiple/albion_cabbage.jpg b/002_match_multiple/albion_cabbage.jpg deleted file mode 100644 index 2c50ef1..0000000 Binary files a/002_match_multiple/albion_cabbage.jpg and /dev/null differ diff --git a/002_match_multiple/albion_copper.jpg b/002_match_multiple/albion_copper.jpg deleted file mode 100644 index 9ba2dbf..0000000 Binary files a/002_match_multiple/albion_copper.jpg and /dev/null differ diff --git a/002_match_multiple/albion_copper_needle.jpg b/002_match_multiple/albion_copper_needle.jpg deleted file mode 100644 index 2baca29..0000000 Binary files a/002_match_multiple/albion_copper_needle.jpg and /dev/null differ diff --git a/002_match_multiple/albion_farm.jpg b/002_match_multiple/albion_farm.jpg deleted file mode 100644 index bcd1e45..0000000 Binary files a/002_match_multiple/albion_farm.jpg and /dev/null differ diff --git a/002_match_multiple/main.py b/002_match_multiple/main.py deleted file mode 100644 index d5a2ba3..0000000 --- a/002_match_multiple/main.py +++ /dev/null @@ -1,53 +0,0 @@ -import cv2 as cv -import numpy as np -import os - - -# Change the working directory to the folder this script is in. -# Doing this because I'll be putting the files from each video in their own folder on GitHub -os.chdir(os.path.dirname(os.path.abspath(__file__))) - -# Can use IMREAD flags to do different pre-processing of image files, -# like making them grayscale or reducing the size. -# https://docs.opencv.org/4.2.0/d4/da8/group__imgcodecs.html -haystack_img = cv.imread('albion_farm.jpg', cv.IMREAD_UNCHANGED) -needle_img = cv.imread('albion_cabbage.jpg', cv.IMREAD_UNCHANGED) - -# There are 6 comparison methods to choose from: -# TM_CCOEFF, TM_CCOEFF_NORMED, TM_CCORR, TM_CCORR_NORMED, TM_SQDIFF, TM_SQDIFF_NORMED -# You can see the differences at a glance here: -# https://docs.opencv.org/master/d4/dc6/tutorial_py_template_matching.html -# Note that the values are inverted for TM_SQDIFF and TM_SQDIFF_NORMED -result = cv.matchTemplate(haystack_img, needle_img, cv.TM_SQDIFF_NORMED) - -# I've inverted the threshold and where comparison to work with TM_SQDIFF_NORMED -threshold = 0.17 -# The np.where() return value will look like this: -# (array([482, 483, 483, 483, 484], dtype=int32), array([514, 513, 514, 515, 514], dtype=int32)) -locations = np.where(result <= threshold) -# We can zip those up into a list of (x, y) position tuples -locations = list(zip(*locations[::-1])) -print(locations) - -if locations: - print('Found needle.') - - needle_w = needle_img.shape[1] - needle_h = needle_img.shape[0] - line_color = (0, 255, 0) - line_type = cv.LINE_4 - - # Loop over all the locations and draw their rectangle - for loc in locations: - # Determine the box positions - top_left = loc - bottom_right = (top_left[0] + needle_w, top_left[1] + needle_h) - # Draw the box - cv.rectangle(haystack_img, top_left, bottom_right, line_color, line_type) - - cv.imshow('Matches', haystack_img) - cv.waitKey() - #cv.imwrite('result.jpg', haystack_img) - -else: - print('Needle not found.') diff --git a/002_match_multiple/result.jpg b/002_match_multiple/result.jpg deleted file mode 100644 index 4967ad9..0000000 Binary files a/002_match_multiple/result.jpg and /dev/null differ diff --git a/003_group_rectangles/albion_cabbage.jpg b/003_group_rectangles/albion_cabbage.jpg deleted file mode 100644 index 2c50ef1..0000000 Binary files a/003_group_rectangles/albion_cabbage.jpg and /dev/null differ diff --git a/003_group_rectangles/albion_farm.jpg b/003_group_rectangles/albion_farm.jpg deleted file mode 100644 index bcd1e45..0000000 Binary files a/003_group_rectangles/albion_farm.jpg and /dev/null differ diff --git a/003_group_rectangles/albion_turnip.jpg b/003_group_rectangles/albion_turnip.jpg deleted file mode 100644 index e6c86ad..0000000 Binary files a/003_group_rectangles/albion_turnip.jpg and /dev/null differ diff --git a/003_group_rectangles/main.py b/003_group_rectangles/main.py deleted file mode 100644 index dba953d..0000000 --- a/003_group_rectangles/main.py +++ /dev/null @@ -1,91 +0,0 @@ -import cv2 as cv -import numpy as np -import os - - -# Change the working directory to the folder this script is in. -# Doing this because I'll be putting the files from each video in their own folder on GitHub -os.chdir(os.path.dirname(os.path.abspath(__file__))) - - -def findClickPositions(needle_img_path, haystack_img_path, threshold=0.5, debug_mode=None): - - # https://docs.opencv.org/4.2.0/d4/da8/group__imgcodecs.html - haystack_img = cv.imread(haystack_img_path, cv.IMREAD_UNCHANGED) - needle_img = cv.imread(needle_img_path, cv.IMREAD_UNCHANGED) - # Save the dimensions of the needle image - needle_w = needle_img.shape[1] - needle_h = needle_img.shape[0] - - # There are 6 methods to choose from: - # TM_CCOEFF, TM_CCOEFF_NORMED, TM_CCORR, TM_CCORR_NORMED, TM_SQDIFF, TM_SQDIFF_NORMED - method = cv.TM_CCOEFF_NORMED - result = cv.matchTemplate(haystack_img, needle_img, method) - - # Get the all the positions from the match result that exceed our threshold - locations = np.where(result >= threshold) - locations = list(zip(*locations[::-1])) - #print(locations) - - # You'll notice a lot of overlapping rectangles get drawn. We can eliminate those redundant - # locations by using groupRectangles(). - # First we need to create the list of [x, y, w, h] rectangles - rectangles = [] - for loc in locations: - rect = [int(loc[0]), int(loc[1]), needle_w, needle_h] - # Add every box to the list twice in order to retain single (non-overlapping) boxes - rectangles.append(rect) - rectangles.append(rect) - # Apply group rectangles. - # The groupThreshold parameter should usually be 1. If you put it at 0 then no grouping is - # done. If you put it at 2 then an object needs at least 3 overlapping rectangles to appear - # in the result. I've set eps to 0.5, which is: - # "Relative difference between sides of the rectangles to merge them into a group." - rectangles, weights = cv.groupRectangles(rectangles, groupThreshold=1, eps=0.5) - #print(rectangles) - - points = [] - if len(rectangles): - #print('Found needle.') - - line_color = (0, 255, 0) - line_type = cv.LINE_4 - marker_color = (255, 0, 255) - marker_type = cv.MARKER_CROSS - - # Loop over all the rectangles - for (x, y, w, h) in rectangles: - - # Determine the center position - center_x = x + int(w/2) - center_y = y + int(h/2) - # Save the points - points.append((center_x, center_y)) - - if debug_mode == 'rectangles': - # Determine the box position - top_left = (x, y) - bottom_right = (x + w, y + h) - # Draw the box - cv.rectangle(haystack_img, top_left, bottom_right, color=line_color, - lineType=line_type, thickness=2) - elif debug_mode == 'points': - # Draw the center point - cv.drawMarker(haystack_img, (center_x, center_y), - color=marker_color, markerType=marker_type, - markerSize=40, thickness=2) - - if debug_mode: - cv.imshow('Matches', haystack_img) - cv.waitKey() - #cv.imwrite('result_click_point.jpg', haystack_img) - - return points - - -points = findClickPositions('albion_cabbage.jpg', 'albion_farm.jpg', debug_mode='points') -print(points) -points = findClickPositions('albion_turnip.jpg', 'albion_farm.jpg', - threshold=0.70, debug_mode='rectangles') -print(points) -print('Done.') diff --git a/003_group_rectangles/result_click_point.jpg b/003_group_rectangles/result_click_point.jpg deleted file mode 100644 index 83a478c..0000000 Binary files a/003_group_rectangles/result_click_point.jpg and /dev/null differ diff --git a/004_window_capture/main.py b/004_window_capture/main.py deleted file mode 100644 index db02537..0000000 --- a/004_window_capture/main.py +++ /dev/null @@ -1,33 +0,0 @@ -import cv2 as cv -import numpy as np -import os -from time import time -from windowcapture import WindowCapture - -# Change the working directory to the folder this script is in. -# Doing this because I'll be putting the files from each video in their own folder on GitHub -os.chdir(os.path.dirname(os.path.abspath(__file__))) - - -# initialize the WindowCapture class -wincap = WindowCapture('Albion Online Client') - -loop_time = time() -while(True): - - # get an updated image of the game - screenshot = wincap.get_screenshot() - - cv.imshow('Computer Vision', screenshot) - - # debug the loop rate - print('FPS {}'.format(1 / (time() - loop_time))) - loop_time = time() - - # press 'q' with the output window focused to exit. - # waits 1 ms every loop to process key presses - if cv.waitKey(1) == ord('q'): - cv.destroyAllWindows() - break - -print('Done.') diff --git a/004_window_capture/windowcapture.py b/004_window_capture/windowcapture.py deleted file mode 100644 index 8a99eb5..0000000 --- a/004_window_capture/windowcapture.py +++ /dev/null @@ -1,93 +0,0 @@ -import numpy as np -import win32gui, win32ui, win32con - - -class WindowCapture: - - # properties - w = 0 - h = 0 - hwnd = None - cropped_x = 0 - cropped_y = 0 - offset_x = 0 - offset_y = 0 - - # constructor - def __init__(self, window_name): - # find the handle for the window we want to capture - self.hwnd = win32gui.FindWindow(None, window_name) - if not self.hwnd: - raise Exception('Window not found: {}'.format(window_name)) - - # get the window size - window_rect = win32gui.GetWindowRect(self.hwnd) - self.w = window_rect[2] - window_rect[0] - self.h = window_rect[3] - window_rect[1] - - # account for the window border and titlebar and cut them off - border_pixels = 8 - titlebar_pixels = 30 - self.w = self.w - (border_pixels * 2) - self.h = self.h - titlebar_pixels - border_pixels - self.cropped_x = border_pixels - self.cropped_y = titlebar_pixels - - # set the cropped coordinates offset so we can translate screenshot - # images into actual screen positions - self.offset_x = window_rect[0] + self.cropped_x - self.offset_y = window_rect[1] + self.cropped_y - - def get_screenshot(self): - - # get the window image data - wDC = win32gui.GetWindowDC(self.hwnd) - dcObj = win32ui.CreateDCFromHandle(wDC) - cDC = dcObj.CreateCompatibleDC() - dataBitMap = win32ui.CreateBitmap() - dataBitMap.CreateCompatibleBitmap(dcObj, self.w, self.h) - cDC.SelectObject(dataBitMap) - cDC.BitBlt((0, 0), (self.w, self.h), dcObj, (self.cropped_x, self.cropped_y), win32con.SRCCOPY) - - # convert the raw data into a format opencv can read - #dataBitMap.SaveBitmapFile(cDC, 'debug.bmp') - signedIntsArray = dataBitMap.GetBitmapBits(True) - img = np.fromstring(signedIntsArray, dtype='uint8') - img.shape = (self.h, self.w, 4) - - # free resources - dcObj.DeleteDC() - cDC.DeleteDC() - win32gui.ReleaseDC(self.hwnd, wDC) - win32gui.DeleteObject(dataBitMap.GetHandle()) - - # drop the alpha channel, or cv.matchTemplate() will throw an error like: - # error: (-215:Assertion failed) (depth == CV_8U || depth == CV_32F) && type == _templ.type() - # && _img.dims() <= 2 in function 'cv::matchTemplate' - img = img[...,:3] - - # make image C_CONTIGUOUS to avoid errors that look like: - # File ... in draw_rectangles - # TypeError: an integer is required (got type tuple) - # see the discussion here: - # https://github.com/opencv/opencv/issues/14866#issuecomment-580207109 - img = np.ascontiguousarray(img) - - return img - - # find the name of the window you're interested in. - # once you have it, update window_capture() - # https://stackoverflow.com/questions/55547940/how-to-get-a-list-of-the-name-of-every-open-window - def list_window_names(self): - def winEnumHandler(hwnd, ctx): - if win32gui.IsWindowVisible(hwnd): - print(hex(hwnd), win32gui.GetWindowText(hwnd)) - win32gui.EnumWindows(winEnumHandler, None) - - # translate a pixel position on a screenshot image to a pixel position on the screen. - # pos = (x, y) - # WARNING: if you move the window being captured after execution is started, this will - # return incorrect coordinates, because the window position is only calculated in - # the __init__ constructor. - def get_screen_position(self, pos): - return (pos[0] + self.offset_x, pos[1] + self.offset_y) diff --git a/005_real_time/albion_limestone.jpg b/005_real_time/albion_limestone.jpg deleted file mode 100644 index 89b910b..0000000 Binary files a/005_real_time/albion_limestone.jpg and /dev/null differ diff --git a/005_real_time/gunsnbottle.jpg b/005_real_time/gunsnbottle.jpg deleted file mode 100644 index 2ab27d6..0000000 Binary files a/005_real_time/gunsnbottle.jpg and /dev/null differ diff --git a/005_real_time/main.py b/005_real_time/main.py deleted file mode 100644 index 793063a..0000000 --- a/005_real_time/main.py +++ /dev/null @@ -1,44 +0,0 @@ -import cv2 as cv -import numpy as np -import os -from time import time -from windowcapture import WindowCapture -from vision import Vision - -# Change the working directory to the folder this script is in. -# Doing this because I'll be putting the files from each video in their own folder on GitHub -os.chdir(os.path.dirname(os.path.abspath(__file__))) - - -# initialize the WindowCapture class -wincap = WindowCapture('Albion Online Client') -# initialize the Vision class -vision_limestone = Vision('albion_limestone.jpg') - -''' -# https://www.crazygames.com/game/guns-and-bottle -wincap = WindowCapture() -vision_gunsnbottle = Vision('gunsnbottle.jpg') -''' - -loop_time = time() -while(True): - - # get an updated image of the game - screenshot = wincap.get_screenshot() - - # display the processed image - points = vision_limestone.find(screenshot, 0.5, 'rectangles') - #points = vision_gunsnbottle.find(screenshot, 0.7, 'points') - - # debug the loop rate - print('FPS {}'.format(1 / (time() - loop_time))) - loop_time = time() - - # press 'q' with the output window focused to exit. - # waits 1 ms every loop to process key presses - if cv.waitKey(1) == ord('q'): - cv.destroyAllWindows() - break - -print('Done.') diff --git a/005_real_time/vision.py b/005_real_time/vision.py deleted file mode 100644 index 371c997..0000000 --- a/005_real_time/vision.py +++ /dev/null @@ -1,89 +0,0 @@ -import cv2 as cv -import numpy as np - - -class Vision: - - # properties - needle_img = None - needle_w = 0 - needle_h = 0 - method = None - - # constructor - def __init__(self, needle_img_path, method=cv.TM_CCOEFF_NORMED): - # load the image we're trying to match - # https://docs.opencv.org/4.2.0/d4/da8/group__imgcodecs.html - self.needle_img = cv.imread(needle_img_path, cv.IMREAD_UNCHANGED) - - # Save the dimensions of the needle image - self.needle_w = self.needle_img.shape[1] - self.needle_h = self.needle_img.shape[0] - - # There are 6 methods to choose from: - # TM_CCOEFF, TM_CCOEFF_NORMED, TM_CCORR, TM_CCORR_NORMED, TM_SQDIFF, TM_SQDIFF_NORMED - self.method = method - - def find(self, haystack_img, threshold=0.5, debug_mode=None): - # run the OpenCV algorithm - result = cv.matchTemplate(haystack_img, self.needle_img, self.method) - - # Get the all the positions from the match result that exceed our threshold - locations = np.where(result >= threshold) - locations = list(zip(*locations[::-1])) - #print(locations) - - # You'll notice a lot of overlapping rectangles get drawn. We can eliminate those redundant - # locations by using groupRectangles(). - # First we need to create the list of [x, y, w, h] rectangles - rectangles = [] - for loc in locations: - rect = [int(loc[0]), int(loc[1]), self.needle_w, self.needle_h] - # Add every box to the list twice in order to retain single (non-overlapping) boxes - rectangles.append(rect) - rectangles.append(rect) - # Apply group rectangles. - # The groupThreshold parameter should usually be 1. If you put it at 0 then no grouping is - # done. If you put it at 2 then an object needs at least 3 overlapping rectangles to appear - # in the result. I've set eps to 0.5, which is: - # "Relative difference between sides of the rectangles to merge them into a group." - rectangles, weights = cv.groupRectangles(rectangles, groupThreshold=1, eps=0.5) - #print(rectangles) - - points = [] - if len(rectangles): - #print('Found needle.') - - line_color = (0, 255, 0) - line_type = cv.LINE_4 - marker_color = (255, 0, 255) - marker_type = cv.MARKER_CROSS - - # Loop over all the rectangles - for (x, y, w, h) in rectangles: - - # Determine the center position - center_x = x + int(w/2) - center_y = y + int(h/2) - # Save the points - points.append((center_x, center_y)) - - if debug_mode == 'rectangles': - # Determine the box position - top_left = (x, y) - bottom_right = (x + w, y + h) - # Draw the box - cv.rectangle(haystack_img, top_left, bottom_right, color=line_color, - lineType=line_type, thickness=2) - elif debug_mode == 'points': - # Draw the center point - cv.drawMarker(haystack_img, (center_x, center_y), - color=marker_color, markerType=marker_type, - markerSize=40, thickness=2) - - if debug_mode: - cv.imshow('Matches', haystack_img) - #cv.waitKey() - #cv.imwrite('result_click_point.jpg', haystack_img) - - return points diff --git a/005_real_time/windowcapture.py b/005_real_time/windowcapture.py deleted file mode 100644 index 0919856..0000000 --- a/005_real_time/windowcapture.py +++ /dev/null @@ -1,98 +0,0 @@ -import numpy as np -import win32gui, win32ui, win32con - - -class WindowCapture: - - # properties - w = 0 - h = 0 - hwnd = None - cropped_x = 0 - cropped_y = 0 - offset_x = 0 - offset_y = 0 - - # constructor - def __init__(self, window_name=None): - # find the handle for the window we want to capture. - # if no window name is given, capture the entire screen - if window_name is None: - self.hwnd = win32gui.GetDesktopWindow() - else: - self.hwnd = win32gui.FindWindow(None, window_name) - if not self.hwnd: - raise Exception('Window not found: {}'.format(window_name)) - - # get the window size - window_rect = win32gui.GetWindowRect(self.hwnd) - self.w = window_rect[2] - window_rect[0] - self.h = window_rect[3] - window_rect[1] - - # account for the window border and titlebar and cut them off - border_pixels = 8 - titlebar_pixels = 30 - self.w = self.w - (border_pixels * 2) - self.h = self.h - titlebar_pixels - border_pixels - self.cropped_x = border_pixels - self.cropped_y = titlebar_pixels - - # set the cropped coordinates offset so we can translate screenshot - # images into actual screen positions - self.offset_x = window_rect[0] + self.cropped_x - self.offset_y = window_rect[1] + self.cropped_y - - def get_screenshot(self): - - # get the window image data - wDC = win32gui.GetWindowDC(self.hwnd) - dcObj = win32ui.CreateDCFromHandle(wDC) - cDC = dcObj.CreateCompatibleDC() - dataBitMap = win32ui.CreateBitmap() - dataBitMap.CreateCompatibleBitmap(dcObj, self.w, self.h) - cDC.SelectObject(dataBitMap) - cDC.BitBlt((0, 0), (self.w, self.h), dcObj, (self.cropped_x, self.cropped_y), win32con.SRCCOPY) - - # convert the raw data into a format opencv can read - #dataBitMap.SaveBitmapFile(cDC, 'debug.bmp') - signedIntsArray = dataBitMap.GetBitmapBits(True) - img = np.fromstring(signedIntsArray, dtype='uint8') - img.shape = (self.h, self.w, 4) - - # free resources - dcObj.DeleteDC() - cDC.DeleteDC() - win32gui.ReleaseDC(self.hwnd, wDC) - win32gui.DeleteObject(dataBitMap.GetHandle()) - - # drop the alpha channel, or cv.matchTemplate() will throw an error like: - # error: (-215:Assertion failed) (depth == CV_8U || depth == CV_32F) && type == _templ.type() - # && _img.dims() <= 2 in function 'cv::matchTemplate' - img = img[...,:3] - - # make image C_CONTIGUOUS to avoid errors that look like: - # File ... in draw_rectangles - # TypeError: an integer is required (got type tuple) - # see the discussion here: - # https://github.com/opencv/opencv/issues/14866#issuecomment-580207109 - img = np.ascontiguousarray(img) - - return img - - # find the name of the window you're interested in. - # once you have it, update window_capture() - # https://stackoverflow.com/questions/55547940/how-to-get-a-list-of-the-name-of-every-open-window - @staticmethod - def list_window_names(): - def winEnumHandler(hwnd, ctx): - if win32gui.IsWindowVisible(hwnd): - print(hex(hwnd), win32gui.GetWindowText(hwnd)) - win32gui.EnumWindows(winEnumHandler, None) - - # translate a pixel position on a screenshot image to a pixel position on the screen. - # pos = (x, y) - # WARNING: if you move the window being captured after execution is started, this will - # return incorrect coordinates, because the window position is only calculated in - # the __init__ constructor. - def get_screen_position(self, pos): - return (pos[0] + self.offset_x, pos[1] + self.offset_y) diff --git a/006_hsv_thresholding/albion_limestone.jpg b/006_hsv_thresholding/albion_limestone.jpg deleted file mode 100644 index 89b910b..0000000 Binary files a/006_hsv_thresholding/albion_limestone.jpg and /dev/null differ diff --git a/006_hsv_thresholding/albion_limestone_processed.jpg b/006_hsv_thresholding/albion_limestone_processed.jpg deleted file mode 100644 index a08e3ef..0000000 Binary files a/006_hsv_thresholding/albion_limestone_processed.jpg and /dev/null differ diff --git a/006_hsv_thresholding/hsvfilter.py b/006_hsv_thresholding/hsvfilter.py deleted file mode 100644 index d537bd2..0000000 --- a/006_hsv_thresholding/hsvfilter.py +++ /dev/null @@ -1,16 +0,0 @@ - -# custom data structure to hold the state of an HSV filter -class HsvFilter: - - def __init__(self, hMin=None, sMin=None, vMin=None, hMax=None, sMax=None, vMax=None, - sAdd=None, sSub=None, vAdd=None, vSub=None): - self.hMin = hMin - self.sMin = sMin - self.vMin = vMin - self.hMax = hMax - self.sMax = sMax - self.vMax = vMax - self.sAdd = sAdd - self.sSub = sSub - self.vAdd = vAdd - self.vSub = vSub diff --git a/006_hsv_thresholding/main.py b/006_hsv_thresholding/main.py deleted file mode 100644 index ba5f2c5..0000000 --- a/006_hsv_thresholding/main.py +++ /dev/null @@ -1,53 +0,0 @@ -import cv2 as cv -import numpy as np -import os -from time import time -from windowcapture import WindowCapture -from vision import Vision -from hsvfilter import HsvFilter - -# Change the working directory to the folder this script is in. -# Doing this because I'll be putting the files from each video in their own folder on GitHub -os.chdir(os.path.dirname(os.path.abspath(__file__))) - - -# initialize the WindowCapture class -wincap = WindowCapture('Albion Online Client') -# initialize the Vision class -vision_limestone = Vision('albion_limestone_processed.jpg') -# initialize the trackbar window -vision_limestone.init_control_gui() - -# limestone HSV filter -hsv_filter = HsvFilter(0, 180, 129, 15, 229, 243, 143, 0, 67, 0) - -loop_time = time() -while(True): - - # get an updated image of the game - screenshot = wincap.get_screenshot() - - # pre-process the image - processed_image = vision_limestone.apply_hsv_filter(screenshot, hsv_filter) - - # do object detection - rectangles = vision_limestone.find(processed_image, 0.46) - - # draw the detection results onto the original image - output_image = vision_limestone.draw_rectangles(screenshot, rectangles) - - # display the processed image - cv.imshow('Processed', processed_image) - cv.imshow('Matches', output_image) - - # debug the loop rate - print('FPS {}'.format(1 / (time() - loop_time))) - loop_time = time() - - # press 'q' with the output window focused to exit. - # waits 1 ms every loop to process key presses - if cv.waitKey(1) == ord('q'): - cv.destroyAllWindows() - break - -print('Done.') diff --git a/006_hsv_thresholding/vision.py b/006_hsv_thresholding/vision.py deleted file mode 100644 index a0f5d60..0000000 --- a/006_hsv_thresholding/vision.py +++ /dev/null @@ -1,204 +0,0 @@ -import cv2 as cv -import numpy as np -from hsvfilter import HsvFilter - - -class Vision: - # constants - TRACKBAR_WINDOW = "Trackbars" - - # properties - needle_img = None - needle_w = 0 - needle_h = 0 - method = None - - # constructor - def __init__(self, needle_img_path, method=cv.TM_CCOEFF_NORMED): - # load the image we're trying to match - # https://docs.opencv.org/4.2.0/d4/da8/group__imgcodecs.html - self.needle_img = cv.imread(needle_img_path, cv.IMREAD_UNCHANGED) - - # Save the dimensions of the needle image - self.needle_w = self.needle_img.shape[1] - self.needle_h = self.needle_img.shape[0] - - # There are 6 methods to choose from: - # TM_CCOEFF, TM_CCOEFF_NORMED, TM_CCORR, TM_CCORR_NORMED, TM_SQDIFF, TM_SQDIFF_NORMED - self.method = method - - def find(self, haystack_img, threshold=0.5, max_results=10): - # run the OpenCV algorithm - result = cv.matchTemplate(haystack_img, self.needle_img, self.method) - - # Get the all the positions from the match result that exceed our threshold - locations = np.where(result >= threshold) - locations = list(zip(*locations[::-1])) - #print(locations) - - # if we found no results, return now. this reshape of the empty array allows us to - # concatenate together results without causing an error - if not locations: - return np.array([], dtype=np.int32).reshape(0, 4) - - # You'll notice a lot of overlapping rectangles get drawn. We can eliminate those redundant - # locations by using groupRectangles(). - # First we need to create the list of [x, y, w, h] rectangles - rectangles = [] - for loc in locations: - rect = [int(loc[0]), int(loc[1]), self.needle_w, self.needle_h] - # Add every box to the list twice in order to retain single (non-overlapping) boxes - rectangles.append(rect) - rectangles.append(rect) - # Apply group rectangles. - # The groupThreshold parameter should usually be 1. If you put it at 0 then no grouping is - # done. If you put it at 2 then an object needs at least 3 overlapping rectangles to appear - # in the result. I've set eps to 0.5, which is: - # "Relative difference between sides of the rectangles to merge them into a group." - rectangles, weights = cv.groupRectangles(rectangles, groupThreshold=1, eps=0.5) - #print(rectangles) - - # for performance reasons, return a limited number of results. - # these aren't necessarily the best results. - if len(rectangles) > max_results: - print('Warning: too many results, raise the threshold.') - rectangles = rectangles[:max_results] - - return rectangles - - # given a list of [x, y, w, h] rectangles returned by find(), convert those into a list of - # [x, y] positions in the center of those rectangles where we can click on those found items - def get_click_points(self, rectangles): - points = [] - - # Loop over all the rectangles - for (x, y, w, h) in rectangles: - # Determine the center position - center_x = x + int(w/2) - center_y = y + int(h/2) - # Save the points - points.append((center_x, center_y)) - - return points - - # given a list of [x, y, w, h] rectangles and a canvas image to draw on, return an image with - # all of those rectangles drawn - def draw_rectangles(self, haystack_img, rectangles): - # these colors are actually BGR - line_color = (0, 255, 0) - line_type = cv.LINE_4 - - for (x, y, w, h) in rectangles: - # determine the box positions - top_left = (x, y) - bottom_right = (x + w, y + h) - # draw the box - cv.rectangle(haystack_img, top_left, bottom_right, line_color, lineType=line_type) - - return haystack_img - - # given a list of [x, y] positions and a canvas image to draw on, return an image with all - # of those click points drawn on as crosshairs - def draw_crosshairs(self, haystack_img, points): - # these colors are actually BGR - marker_color = (255, 0, 255) - marker_type = cv.MARKER_CROSS - - for (center_x, center_y) in points: - # draw the center point - cv.drawMarker(haystack_img, (center_x, center_y), marker_color, marker_type) - - return haystack_img - - # create gui window with controls for adjusting arguments in real-time - def init_control_gui(self): - cv.namedWindow(self.TRACKBAR_WINDOW, cv.WINDOW_NORMAL) - cv.resizeWindow(self.TRACKBAR_WINDOW, 350, 700) - - # required callback. we'll be using getTrackbarPos() to do lookups - # instead of using the callback. - def nothing(position): - pass - - # create trackbars for bracketing. - # OpenCV scale for HSV is H: 0-179, S: 0-255, V: 0-255 - cv.createTrackbar('HMin', self.TRACKBAR_WINDOW, 0, 179, nothing) - cv.createTrackbar('SMin', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('VMin', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('HMax', self.TRACKBAR_WINDOW, 0, 179, nothing) - cv.createTrackbar('SMax', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('VMax', self.TRACKBAR_WINDOW, 0, 255, nothing) - # Set default value for Max HSV trackbars - cv.setTrackbarPos('HMax', self.TRACKBAR_WINDOW, 179) - cv.setTrackbarPos('SMax', self.TRACKBAR_WINDOW, 255) - cv.setTrackbarPos('VMax', self.TRACKBAR_WINDOW, 255) - - # trackbars for increasing/decreasing saturation and value - cv.createTrackbar('SAdd', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('SSub', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('VAdd', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('VSub', self.TRACKBAR_WINDOW, 0, 255, nothing) - - # returns an HSV filter object based on the control GUI values - def get_hsv_filter_from_controls(self): - # Get current positions of all trackbars - hsv_filter = HsvFilter() - hsv_filter.hMin = cv.getTrackbarPos('HMin', self.TRACKBAR_WINDOW) - hsv_filter.sMin = cv.getTrackbarPos('SMin', self.TRACKBAR_WINDOW) - hsv_filter.vMin = cv.getTrackbarPos('VMin', self.TRACKBAR_WINDOW) - hsv_filter.hMax = cv.getTrackbarPos('HMax', self.TRACKBAR_WINDOW) - hsv_filter.sMax = cv.getTrackbarPos('SMax', self.TRACKBAR_WINDOW) - hsv_filter.vMax = cv.getTrackbarPos('VMax', self.TRACKBAR_WINDOW) - hsv_filter.sAdd = cv.getTrackbarPos('SAdd', self.TRACKBAR_WINDOW) - hsv_filter.sSub = cv.getTrackbarPos('SSub', self.TRACKBAR_WINDOW) - hsv_filter.vAdd = cv.getTrackbarPos('VAdd', self.TRACKBAR_WINDOW) - hsv_filter.vSub = cv.getTrackbarPos('VSub', self.TRACKBAR_WINDOW) - return hsv_filter - - # given an image and an HSV filter, apply the filter and return the resulting image. - # if a filter is not supplied, the control GUI trackbars will be used - def apply_hsv_filter(self, original_image, hsv_filter=None): - # convert image to HSV - hsv = cv.cvtColor(original_image, cv.COLOR_BGR2HSV) - - # if we haven't been given a defined filter, use the filter values from the GUI - if not hsv_filter: - hsv_filter = self.get_hsv_filter_from_controls() - - # add/subtract saturation and value - h, s, v = cv.split(hsv) - s = self.shift_channel(s, hsv_filter.sAdd) - s = self.shift_channel(s, -hsv_filter.sSub) - v = self.shift_channel(v, hsv_filter.vAdd) - v = self.shift_channel(v, -hsv_filter.vSub) - hsv = cv.merge([h, s, v]) - - # Set minimum and maximum HSV values to display - lower = np.array([hsv_filter.hMin, hsv_filter.sMin, hsv_filter.vMin]) - upper = np.array([hsv_filter.hMax, hsv_filter.sMax, hsv_filter.vMax]) - # Apply the thresholds - mask = cv.inRange(hsv, lower, upper) - result = cv.bitwise_and(hsv, hsv, mask=mask) - - # convert back to BGR for imshow() to display it properly - img = cv.cvtColor(result, cv.COLOR_HSV2BGR) - - return img - - # apply adjustments to an HSV channel - # https://stackoverflow.com/questions/49697363/shifting-hsv-pixel-values-in-python-using-numpy - def shift_channel(self, c, amount): - if amount > 0: - lim = 255 - amount - c[c >= lim] = 255 - c[c < lim] += amount - elif amount < 0: - amount = -amount - lim = amount - c[c <= lim] = 0 - c[c > lim] -= amount - return c - - - - diff --git a/006_hsv_thresholding/windowcapture.py b/006_hsv_thresholding/windowcapture.py deleted file mode 100644 index 0919856..0000000 --- a/006_hsv_thresholding/windowcapture.py +++ /dev/null @@ -1,98 +0,0 @@ -import numpy as np -import win32gui, win32ui, win32con - - -class WindowCapture: - - # properties - w = 0 - h = 0 - hwnd = None - cropped_x = 0 - cropped_y = 0 - offset_x = 0 - offset_y = 0 - - # constructor - def __init__(self, window_name=None): - # find the handle for the window we want to capture. - # if no window name is given, capture the entire screen - if window_name is None: - self.hwnd = win32gui.GetDesktopWindow() - else: - self.hwnd = win32gui.FindWindow(None, window_name) - if not self.hwnd: - raise Exception('Window not found: {}'.format(window_name)) - - # get the window size - window_rect = win32gui.GetWindowRect(self.hwnd) - self.w = window_rect[2] - window_rect[0] - self.h = window_rect[3] - window_rect[1] - - # account for the window border and titlebar and cut them off - border_pixels = 8 - titlebar_pixels = 30 - self.w = self.w - (border_pixels * 2) - self.h = self.h - titlebar_pixels - border_pixels - self.cropped_x = border_pixels - self.cropped_y = titlebar_pixels - - # set the cropped coordinates offset so we can translate screenshot - # images into actual screen positions - self.offset_x = window_rect[0] + self.cropped_x - self.offset_y = window_rect[1] + self.cropped_y - - def get_screenshot(self): - - # get the window image data - wDC = win32gui.GetWindowDC(self.hwnd) - dcObj = win32ui.CreateDCFromHandle(wDC) - cDC = dcObj.CreateCompatibleDC() - dataBitMap = win32ui.CreateBitmap() - dataBitMap.CreateCompatibleBitmap(dcObj, self.w, self.h) - cDC.SelectObject(dataBitMap) - cDC.BitBlt((0, 0), (self.w, self.h), dcObj, (self.cropped_x, self.cropped_y), win32con.SRCCOPY) - - # convert the raw data into a format opencv can read - #dataBitMap.SaveBitmapFile(cDC, 'debug.bmp') - signedIntsArray = dataBitMap.GetBitmapBits(True) - img = np.fromstring(signedIntsArray, dtype='uint8') - img.shape = (self.h, self.w, 4) - - # free resources - dcObj.DeleteDC() - cDC.DeleteDC() - win32gui.ReleaseDC(self.hwnd, wDC) - win32gui.DeleteObject(dataBitMap.GetHandle()) - - # drop the alpha channel, or cv.matchTemplate() will throw an error like: - # error: (-215:Assertion failed) (depth == CV_8U || depth == CV_32F) && type == _templ.type() - # && _img.dims() <= 2 in function 'cv::matchTemplate' - img = img[...,:3] - - # make image C_CONTIGUOUS to avoid errors that look like: - # File ... in draw_rectangles - # TypeError: an integer is required (got type tuple) - # see the discussion here: - # https://github.com/opencv/opencv/issues/14866#issuecomment-580207109 - img = np.ascontiguousarray(img) - - return img - - # find the name of the window you're interested in. - # once you have it, update window_capture() - # https://stackoverflow.com/questions/55547940/how-to-get-a-list-of-the-name-of-every-open-window - @staticmethod - def list_window_names(): - def winEnumHandler(hwnd, ctx): - if win32gui.IsWindowVisible(hwnd): - print(hex(hwnd), win32gui.GetWindowText(hwnd)) - win32gui.EnumWindows(winEnumHandler, None) - - # translate a pixel position on a screenshot image to a pixel position on the screen. - # pos = (x, y) - # WARNING: if you move the window being captured after execution is started, this will - # return incorrect coordinates, because the window position is only calculated in - # the __init__ constructor. - def get_screen_position(self, pos): - return (pos[0] + self.offset_x, pos[1] + self.offset_y) diff --git a/007_canny_edge/albion_limestone.jpg b/007_canny_edge/albion_limestone.jpg deleted file mode 100644 index f839119..0000000 Binary files a/007_canny_edge/albion_limestone.jpg and /dev/null differ diff --git a/007_canny_edge/albion_limestone_edges.jpg b/007_canny_edge/albion_limestone_edges.jpg deleted file mode 100644 index 0389c31..0000000 Binary files a/007_canny_edge/albion_limestone_edges.jpg and /dev/null differ diff --git a/007_canny_edge/albion_limestone_processed.jpg b/007_canny_edge/albion_limestone_processed.jpg deleted file mode 100644 index a08e3ef..0000000 Binary files a/007_canny_edge/albion_limestone_processed.jpg and /dev/null differ diff --git a/007_canny_edge/edgefilter.py b/007_canny_edge/edgefilter.py deleted file mode 100644 index 6e25005..0000000 --- a/007_canny_edge/edgefilter.py +++ /dev/null @@ -1,11 +0,0 @@ - -# custom data structure to hold the state of a Canny edge filter -class EdgeFilter: - - def __init__(self, kernelSize=None, erodeIter=None, dilateIter=None, canny1=None, - canny2=None): - self.kernelSize = kernelSize - self.erodeIter = erodeIter - self.dilateIter = dilateIter - self.canny1 = canny1 - self.canny2 = canny2 diff --git a/007_canny_edge/hsvfilter.py b/007_canny_edge/hsvfilter.py deleted file mode 100644 index d537bd2..0000000 --- a/007_canny_edge/hsvfilter.py +++ /dev/null @@ -1,16 +0,0 @@ - -# custom data structure to hold the state of an HSV filter -class HsvFilter: - - def __init__(self, hMin=None, sMin=None, vMin=None, hMax=None, sMax=None, vMax=None, - sAdd=None, sSub=None, vAdd=None, vSub=None): - self.hMin = hMin - self.sMin = sMin - self.vMin = vMin - self.hMax = hMax - self.sMax = sMax - self.vMax = vMax - self.sAdd = sAdd - self.sSub = sSub - self.vAdd = vAdd - self.vSub = vSub diff --git a/007_canny_edge/main.py b/007_canny_edge/main.py deleted file mode 100644 index 346a213..0000000 --- a/007_canny_edge/main.py +++ /dev/null @@ -1,82 +0,0 @@ -import cv2 as cv -import numpy as np -import os -from time import time -from windowcapture import WindowCapture -from vision import Vision -from hsvfilter import HsvFilter -from edgefilter import EdgeFilter - -# Change the working directory to the folder this script is in. -# Doing this because I'll be putting the files from each video in their own folder on GitHub -os.chdir(os.path.dirname(os.path.abspath(__file__))) - - -# initialize the WindowCapture class -wincap = WindowCapture('Albion Online Client') -# initialize the Vision class -vision_limestone = Vision('albion_limestone_edges.jpg') -# initialize the trackbar window -vision_limestone.init_control_gui() - -# limestone HSV filter -hsv_filter = HsvFilter(0, 180, 129, 15, 229, 243, 143, 0, 67, 0) - -loop_time = time() -while(True): - - # get an updated image of the game - screenshot = wincap.get_screenshot() - - # pre-process the image - processed_image = vision_limestone.apply_hsv_filter(screenshot) - - # do edge detection - edges_image = vision_limestone.apply_edge_filter(processed_image) - - # do object detection - #rectangles = vision_limestone.find(processed_image, 0.46) - - # draw the detection results onto the original image - #output_image = vision_limestone.draw_rectangles(screenshot, rectangles) - - # keypoint searching - keypoint_image = edges_image - # crop the image to remove the ui elements - x, w, y, h = [200, 1130, 70, 750] - keypoint_image = keypoint_image[y:y+h, x:x+w] - - kp1, kp2, matches, match_points = vision_limestone.match_keypoints(keypoint_image) - match_image = cv.drawMatches( - vision_limestone.needle_img, - kp1, - keypoint_image, - kp2, - matches, - None) - - if match_points: - # find the center point of all the matched features - center_point = vision_limestone.centeroid(match_points) - # account for the width of the needle image that appears on the left - center_point[0] += vision_limestone.needle_w - # drawn the found center point on the output image - match_image = vision_limestone.draw_crosshairs(match_image, [center_point]) - - # display the processed image - cv.imshow('Keypoint Search', match_image) - cv.imshow('Processed', processed_image) - cv.imshow('Edges', edges_image) - #cv.imshow('Matches', output_image) - - # debug the loop rate - print('FPS {}'.format(1 / (time() - loop_time))) - loop_time = time() - - # press 'q' with the output window focused to exit. - # waits 1 ms every loop to process key presses - if cv.waitKey(1) == ord('q'): - cv.destroyAllWindows() - break - -print('Done.') diff --git a/007_canny_edge/vision.py b/007_canny_edge/vision.py deleted file mode 100644 index 2462070..0000000 --- a/007_canny_edge/vision.py +++ /dev/null @@ -1,288 +0,0 @@ -import cv2 as cv -import numpy as np -from hsvfilter import HsvFilter -from edgefilter import EdgeFilter - - -class Vision: - # constants - TRACKBAR_WINDOW = "Trackbars" - - # properties - needle_img = None - needle_w = 0 - needle_h = 0 - method = None - - # constructor - def __init__(self, needle_img_path, method=cv.TM_CCOEFF_NORMED): - # load the image we're trying to match - # https://docs.opencv.org/4.2.0/d4/da8/group__imgcodecs.html - self.needle_img = cv.imread(needle_img_path, cv.IMREAD_UNCHANGED) - - # Save the dimensions of the needle image - self.needle_w = self.needle_img.shape[1] - self.needle_h = self.needle_img.shape[0] - - # There are 6 methods to choose from: - # TM_CCOEFF, TM_CCOEFF_NORMED, TM_CCORR, TM_CCORR_NORMED, TM_SQDIFF, TM_SQDIFF_NORMED - self.method = method - - def find(self, haystack_img, threshold=0.5, max_results=10): - # run the OpenCV algorithm - result = cv.matchTemplate(haystack_img, self.needle_img, self.method) - - # Get the all the positions from the match result that exceed our threshold - locations = np.where(result >= threshold) - locations = list(zip(*locations[::-1])) - #print(locations) - - # if we found no results, return now. this reshape of the empty array allows us to - # concatenate together results without causing an error - if not locations: - return np.array([], dtype=np.int32).reshape(0, 4) - - # You'll notice a lot of overlapping rectangles get drawn. We can eliminate those redundant - # locations by using groupRectangles(). - # First we need to create the list of [x, y, w, h] rectangles - rectangles = [] - for loc in locations: - rect = [int(loc[0]), int(loc[1]), self.needle_w, self.needle_h] - # Add every box to the list twice in order to retain single (non-overlapping) boxes - rectangles.append(rect) - rectangles.append(rect) - # Apply group rectangles. - # The groupThreshold parameter should usually be 1. If you put it at 0 then no grouping is - # done. If you put it at 2 then an object needs at least 3 overlapping rectangles to appear - # in the result. I've set eps to 0.5, which is: - # "Relative difference between sides of the rectangles to merge them into a group." - rectangles, weights = cv.groupRectangles(rectangles, groupThreshold=1, eps=0.5) - #print(rectangles) - - # for performance reasons, return a limited number of results. - # these aren't necessarily the best results. - if len(rectangles) > max_results: - print('Warning: too many results, raise the threshold.') - rectangles = rectangles[:max_results] - - return rectangles - - # given a list of [x, y, w, h] rectangles returned by find(), convert those into a list of - # [x, y] positions in the center of those rectangles where we can click on those found items - def get_click_points(self, rectangles): - points = [] - - # Loop over all the rectangles - for (x, y, w, h) in rectangles: - # Determine the center position - center_x = x + int(w/2) - center_y = y + int(h/2) - # Save the points - points.append((center_x, center_y)) - - return points - - # given a list of [x, y, w, h] rectangles and a canvas image to draw on, return an image with - # all of those rectangles drawn - def draw_rectangles(self, haystack_img, rectangles): - # these colors are actually BGR - line_color = (0, 255, 0) - line_type = cv.LINE_4 - - for (x, y, w, h) in rectangles: - # determine the box positions - top_left = (x, y) - bottom_right = (x + w, y + h) - # draw the box - cv.rectangle(haystack_img, top_left, bottom_right, line_color, lineType=line_type) - - return haystack_img - - # given a list of [x, y] positions and a canvas image to draw on, return an image with all - # of those click points drawn on as crosshairs - def draw_crosshairs(self, haystack_img, points): - # these colors are actually BGR - marker_color = (255, 0, 255) - marker_type = cv.MARKER_CROSS - - for (center_x, center_y) in points: - # draw the center point - cv.drawMarker(haystack_img, (center_x, center_y), marker_color, marker_type) - - return haystack_img - - # create gui window with controls for adjusting arguments in real-time - def init_control_gui(self): - cv.namedWindow(self.TRACKBAR_WINDOW, cv.WINDOW_NORMAL) - cv.resizeWindow(self.TRACKBAR_WINDOW, 350, 700) - - # required callback. we'll be using getTrackbarPos() to do lookups - # instead of using the callback. - def nothing(position): - pass - - # create trackbars for bracketing. - # OpenCV scale for HSV is H: 0-179, S: 0-255, V: 0-255 - cv.createTrackbar('HMin', self.TRACKBAR_WINDOW, 0, 179, nothing) - cv.createTrackbar('SMin', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('VMin', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('HMax', self.TRACKBAR_WINDOW, 0, 179, nothing) - cv.createTrackbar('SMax', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('VMax', self.TRACKBAR_WINDOW, 0, 255, nothing) - # Set default value for Max HSV trackbars - cv.setTrackbarPos('HMax', self.TRACKBAR_WINDOW, 179) - cv.setTrackbarPos('SMax', self.TRACKBAR_WINDOW, 255) - cv.setTrackbarPos('VMax', self.TRACKBAR_WINDOW, 255) - - # trackbars for increasing/decreasing saturation and value - cv.createTrackbar('SAdd', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('SSub', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('VAdd', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('VSub', self.TRACKBAR_WINDOW, 0, 255, nothing) - - # trackbars for edge creation - cv.createTrackbar('KernelSize', self.TRACKBAR_WINDOW, 1, 30, nothing) - cv.createTrackbar('ErodeIter', self.TRACKBAR_WINDOW, 1, 5, nothing) - cv.createTrackbar('DilateIter', self.TRACKBAR_WINDOW, 1, 5, nothing) - cv.createTrackbar('Canny1', self.TRACKBAR_WINDOW, 0, 200, nothing) - cv.createTrackbar('Canny2', self.TRACKBAR_WINDOW, 0, 500, nothing) - # Set default value for Canny trackbars - cv.setTrackbarPos('KernelSize', self.TRACKBAR_WINDOW, 5) - cv.setTrackbarPos('Canny1', self.TRACKBAR_WINDOW, 100) - cv.setTrackbarPos('Canny2', self.TRACKBAR_WINDOW, 200) - - # returns an HSV filter object based on the control GUI values - def get_hsv_filter_from_controls(self): - # Get current positions of all trackbars - hsv_filter = HsvFilter() - hsv_filter.hMin = cv.getTrackbarPos('HMin', self.TRACKBAR_WINDOW) - hsv_filter.sMin = cv.getTrackbarPos('SMin', self.TRACKBAR_WINDOW) - hsv_filter.vMin = cv.getTrackbarPos('VMin', self.TRACKBAR_WINDOW) - hsv_filter.hMax = cv.getTrackbarPos('HMax', self.TRACKBAR_WINDOW) - hsv_filter.sMax = cv.getTrackbarPos('SMax', self.TRACKBAR_WINDOW) - hsv_filter.vMax = cv.getTrackbarPos('VMax', self.TRACKBAR_WINDOW) - hsv_filter.sAdd = cv.getTrackbarPos('SAdd', self.TRACKBAR_WINDOW) - hsv_filter.sSub = cv.getTrackbarPos('SSub', self.TRACKBAR_WINDOW) - hsv_filter.vAdd = cv.getTrackbarPos('VAdd', self.TRACKBAR_WINDOW) - hsv_filter.vSub = cv.getTrackbarPos('VSub', self.TRACKBAR_WINDOW) - return hsv_filter - - # returns a Canny edge filter object based on the control GUI values - def get_edge_filter_from_controls(self): - # Get current positions of all trackbars - edge_filter = EdgeFilter() - edge_filter.kernelSize = cv.getTrackbarPos('KernelSize', self.TRACKBAR_WINDOW) - edge_filter.erodeIter = cv.getTrackbarPos('ErodeIter', self.TRACKBAR_WINDOW) - edge_filter.dilateIter = cv.getTrackbarPos('DilateIter', self.TRACKBAR_WINDOW) - edge_filter.canny1 = cv.getTrackbarPos('Canny1', self.TRACKBAR_WINDOW) - edge_filter.canny2 = cv.getTrackbarPos('Canny2', self.TRACKBAR_WINDOW) - return edge_filter - - # given an image and an HSV filter, apply the filter and return the resulting image. - # if a filter is not supplied, the control GUI trackbars will be used - def apply_hsv_filter(self, original_image, hsv_filter=None): - # convert image to HSV - hsv = cv.cvtColor(original_image, cv.COLOR_BGR2HSV) - - # if we haven't been given a defined filter, use the filter values from the GUI - if not hsv_filter: - hsv_filter = self.get_hsv_filter_from_controls() - - # add/subtract saturation and value - h, s, v = cv.split(hsv) - s = self.shift_channel(s, hsv_filter.sAdd) - s = self.shift_channel(s, -hsv_filter.sSub) - v = self.shift_channel(v, hsv_filter.vAdd) - v = self.shift_channel(v, -hsv_filter.vSub) - hsv = cv.merge([h, s, v]) - - # Set minimum and maximum HSV values to display - lower = np.array([hsv_filter.hMin, hsv_filter.sMin, hsv_filter.vMin]) - upper = np.array([hsv_filter.hMax, hsv_filter.sMax, hsv_filter.vMax]) - # Apply the thresholds - mask = cv.inRange(hsv, lower, upper) - result = cv.bitwise_and(hsv, hsv, mask=mask) - - # convert back to BGR for imshow() to display it properly - img = cv.cvtColor(result, cv.COLOR_HSV2BGR) - - return img - - # given an image and a Canny edge filter, apply the filter and return the resulting image. - # if a filter is not supplied, the control GUI trackbars will be used - def apply_edge_filter(self, original_image, edge_filter=None): - # if we haven't been given a defined filter, use the filter values from the GUI - if not edge_filter: - edge_filter = self.get_edge_filter_from_controls() - - kernel = np.ones((edge_filter.kernelSize, edge_filter.kernelSize), np.uint8) - eroded_image = cv.erode(original_image, kernel, iterations=edge_filter.erodeIter) - dilated_image = cv.dilate(eroded_image, kernel, iterations=edge_filter.dilateIter) - - # canny edge detection - result = cv.Canny(dilated_image, edge_filter.canny1, edge_filter.canny2) - - # convert single channel image back to BGR - img = cv.cvtColor(result, cv.COLOR_GRAY2BGR) - - return img - - # apply adjustments to an HSV channel - # https://stackoverflow.com/questions/49697363/shifting-hsv-pixel-values-in-python-using-numpy - def shift_channel(self, c, amount): - if amount > 0: - lim = 255 - amount - c[c >= lim] = 255 - c[c < lim] += amount - elif amount < 0: - amount = -amount - lim = amount - c[c <= lim] = 0 - c[c > lim] -= amount - return c - - def match_keypoints(self, original_image, patch_size=32): - min_match_count = 5 - - orb = cv.ORB_create(edgeThreshold=0, patchSize=patch_size) - keypoints_needle, descriptors_needle = orb.detectAndCompute(self.needle_img, None) - orb2 = cv.ORB_create(edgeThreshold=0, patchSize=patch_size, nfeatures=2000) - keypoints_haystack, descriptors_haystack = orb2.detectAndCompute(original_image, None) - - FLANN_INDEX_LSH = 6 - index_params = dict(algorithm=FLANN_INDEX_LSH, - table_number=6, - key_size=12, - multi_probe_level=1) - - search_params = dict(checks=50) - - try: - flann = cv.FlannBasedMatcher(index_params, search_params) - matches = flann.knnMatch(descriptors_needle, descriptors_haystack, k=2) - except cv.error: - return None, None, [], [], None - - # store all the good matches as per Lowe's ratio test. - good = [] - points = [] - - for pair in matches: - if len(pair) == 2: - if pair[0].distance < 0.7*pair[1].distance: - good.append(pair[0]) - - if len(good) > min_match_count: - print('match %03d, kp %03d' % (len(good), len(keypoints_needle))) - for match in good: - points.append(keypoints_haystack[match.trainIdx].pt) - #print(points) - - return keypoints_needle, keypoints_haystack, good, points - - def centeroid(self, point_list): - point_list = np.asarray(point_list, dtype=np.int32) - length = point_list.shape[0] - sum_x = np.sum(point_list[:, 0]) - sum_y = np.sum(point_list[:, 1]) - return [np.floor_divide(sum_x, length), np.floor_divide(sum_y, length)] diff --git a/007_canny_edge/windowcapture.py b/007_canny_edge/windowcapture.py deleted file mode 100644 index 0919856..0000000 --- a/007_canny_edge/windowcapture.py +++ /dev/null @@ -1,98 +0,0 @@ -import numpy as np -import win32gui, win32ui, win32con - - -class WindowCapture: - - # properties - w = 0 - h = 0 - hwnd = None - cropped_x = 0 - cropped_y = 0 - offset_x = 0 - offset_y = 0 - - # constructor - def __init__(self, window_name=None): - # find the handle for the window we want to capture. - # if no window name is given, capture the entire screen - if window_name is None: - self.hwnd = win32gui.GetDesktopWindow() - else: - self.hwnd = win32gui.FindWindow(None, window_name) - if not self.hwnd: - raise Exception('Window not found: {}'.format(window_name)) - - # get the window size - window_rect = win32gui.GetWindowRect(self.hwnd) - self.w = window_rect[2] - window_rect[0] - self.h = window_rect[3] - window_rect[1] - - # account for the window border and titlebar and cut them off - border_pixels = 8 - titlebar_pixels = 30 - self.w = self.w - (border_pixels * 2) - self.h = self.h - titlebar_pixels - border_pixels - self.cropped_x = border_pixels - self.cropped_y = titlebar_pixels - - # set the cropped coordinates offset so we can translate screenshot - # images into actual screen positions - self.offset_x = window_rect[0] + self.cropped_x - self.offset_y = window_rect[1] + self.cropped_y - - def get_screenshot(self): - - # get the window image data - wDC = win32gui.GetWindowDC(self.hwnd) - dcObj = win32ui.CreateDCFromHandle(wDC) - cDC = dcObj.CreateCompatibleDC() - dataBitMap = win32ui.CreateBitmap() - dataBitMap.CreateCompatibleBitmap(dcObj, self.w, self.h) - cDC.SelectObject(dataBitMap) - cDC.BitBlt((0, 0), (self.w, self.h), dcObj, (self.cropped_x, self.cropped_y), win32con.SRCCOPY) - - # convert the raw data into a format opencv can read - #dataBitMap.SaveBitmapFile(cDC, 'debug.bmp') - signedIntsArray = dataBitMap.GetBitmapBits(True) - img = np.fromstring(signedIntsArray, dtype='uint8') - img.shape = (self.h, self.w, 4) - - # free resources - dcObj.DeleteDC() - cDC.DeleteDC() - win32gui.ReleaseDC(self.hwnd, wDC) - win32gui.DeleteObject(dataBitMap.GetHandle()) - - # drop the alpha channel, or cv.matchTemplate() will throw an error like: - # error: (-215:Assertion failed) (depth == CV_8U || depth == CV_32F) && type == _templ.type() - # && _img.dims() <= 2 in function 'cv::matchTemplate' - img = img[...,:3] - - # make image C_CONTIGUOUS to avoid errors that look like: - # File ... in draw_rectangles - # TypeError: an integer is required (got type tuple) - # see the discussion here: - # https://github.com/opencv/opencv/issues/14866#issuecomment-580207109 - img = np.ascontiguousarray(img) - - return img - - # find the name of the window you're interested in. - # once you have it, update window_capture() - # https://stackoverflow.com/questions/55547940/how-to-get-a-list-of-the-name-of-every-open-window - @staticmethod - def list_window_names(): - def winEnumHandler(hwnd, ctx): - if win32gui.IsWindowVisible(hwnd): - print(hex(hwnd), win32gui.GetWindowText(hwnd)) - win32gui.EnumWindows(winEnumHandler, None) - - # translate a pixel position on a screenshot image to a pixel position on the screen. - # pos = (x, y) - # WARNING: if you move the window being captured after execution is started, this will - # return incorrect coordinates, because the window position is only calculated in - # the __init__ constructor. - def get_screen_position(self, pos): - return (pos[0] + self.offset_x, pos[1] + self.offset_y) diff --git a/008_cascade_classifier/cascadeutils.py b/008_cascade_classifier/cascadeutils.py deleted file mode 100644 index be3c8c2..0000000 --- a/008_cascade_classifier/cascadeutils.py +++ /dev/null @@ -1,37 +0,0 @@ -import os - - -# reads all the files in the /negative folder and generates neg.txt from them. -# we'll run it manually like this: -# $ python -# Python 3.8.0 (tags/v3.8.0:fa919fd, Oct 14 2019, 19:21:23) [MSC v.1916 32 bit (Intel)] on win32 -# Type "help", "copyright", "credits" or "license" for more information. -# >>> from cascadeutils import generate_negative_description_file -# >>> generate_negative_description_file() -# >>> exit() -def generate_negative_description_file(): - # open the output file for writing. will overwrite all existing data in there - with open('neg.txt', 'w') as f: - # loop over all the filenames - for filename in os.listdir('negative'): - f.write('negative/' + filename + '\n') - -# the opencv_annotation executable can be found in opencv/build/x64/vc15/bin -# generate positive description file using: -# $ C:/Users/Ben/learncodebygaming/opencv/build/x64/vc15/bin/opencv_annotation.exe --annotations=pos.txt --images=positive/ - -# You click once to set the upper left corner, then again to set the lower right corner. -# Press 'c' to confirm. -# Or 'd' to undo the previous confirmation. -# When done, click 'n' to move to the next image. -# Press 'esc' to exit. -# Will exit automatically when you've annotated all of the images - -# generate positive samples from the annotations to get a vector file using: -# $ C:/Users/Ben/learncodebygaming/opencv/build/x64/vc15/bin/opencv_createsamples.exe -info pos.txt -w 24 -h 24 -num 1000 -vec pos.vec - -# train the cascade classifier model using: -# $ C:/Users/Ben/learncodebygaming/opencv/build/x64/vc15/bin/opencv_traincascade.exe -data cascade/ -vec pos.vec -bg neg.txt -numPos 200 -numNeg 100 -numStages 10 -w 24 -h 24 - -# my final classifier training arguments: -# $ C:/Users/Ben/learncodebygaming/opencv/build/x64/vc15/bin/opencv_traincascade.exe -data cascade/ -vec pos.vec -bg neg.txt -precalcValBufSize 6000 -precalcIdxBufSize 6000 -numPos 200 -numNeg 1000 -numStages 12 -w 24 -h 24 -maxFalseAlarmRate 0.4 -minHitRate 0.999 diff --git a/008_cascade_classifier/edgefilter.py b/008_cascade_classifier/edgefilter.py deleted file mode 100644 index 6e25005..0000000 --- a/008_cascade_classifier/edgefilter.py +++ /dev/null @@ -1,11 +0,0 @@ - -# custom data structure to hold the state of a Canny edge filter -class EdgeFilter: - - def __init__(self, kernelSize=None, erodeIter=None, dilateIter=None, canny1=None, - canny2=None): - self.kernelSize = kernelSize - self.erodeIter = erodeIter - self.dilateIter = dilateIter - self.canny1 = canny1 - self.canny2 = canny2 diff --git a/008_cascade_classifier/hsvfilter.py b/008_cascade_classifier/hsvfilter.py deleted file mode 100644 index d537bd2..0000000 --- a/008_cascade_classifier/hsvfilter.py +++ /dev/null @@ -1,16 +0,0 @@ - -# custom data structure to hold the state of an HSV filter -class HsvFilter: - - def __init__(self, hMin=None, sMin=None, vMin=None, hMax=None, sMax=None, vMax=None, - sAdd=None, sSub=None, vAdd=None, vSub=None): - self.hMin = hMin - self.sMin = sMin - self.vMin = vMin - self.hMax = hMax - self.sMax = sMax - self.vMax = vMax - self.sAdd = sAdd - self.sSub = sSub - self.vAdd = vAdd - self.vSub = vSub diff --git a/008_cascade_classifier/limestone_model_final.xml b/008_cascade_classifier/limestone_model_final.xml deleted file mode 100644 index 99a6398..0000000 --- a/008_cascade_classifier/limestone_model_final.xml +++ /dev/null @@ -1,1737 +0,0 @@ - - - - BOOST - HAAR - 24 - 24 - - GAB - 9.9900001287460327e-01 - 4.0000000596046448e-01 - 9.4999999999999996e-01 - 1 - 100 - - 0 - 1 - BASIC - 12 - - - <_> - 3 - -1.2815593481063843e+00 - - <_> - - 0 -1 29 -2.6531535387039185e-01 - - -9.7701150178909302e-01 1.5151515603065491e-01 - <_> - - 0 -1 73 3.3036179840564728e-02 - - -7.2519451379776001e-01 8.4022516012191772e-01 - <_> - - 0 -1 113 -4.1663810610771179e-02 - - -9.3661451339721680e-01 4.2064669728279114e-01 - - <_> - 7 - -1.5778355598449707e+00 - - <_> - - 0 -1 49 1.4340322464704514e-02 - - -8.1261950731277466e-01 3.2467532157897949e-01 - <_> - - 0 -1 6 -7.0590246468782425e-03 - - 6.8404257297515869e-01 -4.9814325571060181e-01 - <_> - - 0 -1 129 1.0002681985497475e-02 - - -3.5684755444526672e-01 7.5740498304367065e-01 - <_> - - 0 -1 14 -2.1922769024968147e-02 - - 7.6397258043289185e-01 -3.1399649381637573e-01 - <_> - - 0 -1 78 -2.2847860236652195e-04 - - 4.7088450193405151e-01 -5.7153099775314331e-01 - <_> - - 0 -1 51 1.2028008699417114e-02 - - -3.4323534369468689e-01 6.4455568790435791e-01 - <_> - - 0 -1 71 -4.8528384417295456e-02 - - -9.2709094285964966e-01 2.7612203359603882e-01 - - <_> - 8 - -1.6270295381546021e+00 - - <_> - - 0 -1 60 5.1783770322799683e-02 - - -8.1818181276321411e-01 4.4444444775581360e-01 - <_> - - 0 -1 126 3.4453733824193478e-03 - - -5.2110916376113892e-01 3.8695532083511353e-01 - <_> - - 0 -1 86 -2.3107819259166718e-02 - - 1.8288460373878479e-01 -9.5233714580535889e-01 - <_> - - 0 -1 9 -8.4966458380222321e-03 - - 6.5764582157135010e-01 -2.9777401685714722e-01 - <_> - - 0 -1 52 5.5540574248880148e-04 - - -2.5696116685867310e-01 5.2375447750091553e-01 - <_> - - 0 -1 110 3.7999920547008514e-02 - - -2.2597408294677734e-01 6.5327495336532593e-01 - <_> - - 0 -1 38 -4.8657096922397614e-02 - - 6.8704473972320557e-01 -2.1762873232364655e-01 - <_> - - 0 -1 34 3.7764841690659523e-03 - - -3.5153421759605408e-01 5.6150972843170166e-01 - - <_> - 13 - -1.5087051391601563e+00 - - <_> - - 0 -1 89 3.6675274372100830e-02 - - -7.9358714818954468e-01 -3.9603959769010544e-02 - <_> - - 0 -1 7 -2.5295931845903397e-02 - - 2.9524472355842590e-01 -5.1345789432525635e-01 - <_> - - 0 -1 30 -2.0656153559684753e-02 - - 4.0355071425437927e-01 -3.2067385315895081e-01 - <_> - - 0 -1 47 1.3974596746265888e-02 - - -2.6310223340988159e-01 5.7496279478073120e-01 - <_> - - 0 -1 128 1.4068853110074997e-02 - - -1.8699720501899719e-01 7.7813905477523804e-01 - <_> - - 0 -1 55 -1.1953005567193031e-02 - - 4.4992053508758545e-01 -2.6490813493728638e-01 - <_> - - 0 -1 119 5.5090682581067085e-03 - - -2.2897675633430481e-01 5.0835984945297241e-01 - <_> - - 0 -1 81 -3.7762962281703949e-02 - - -9.6912604570388794e-01 1.6655203700065613e-01 - <_> - - 0 -1 45 1.9030734896659851e-02 - - -2.2931246459484100e-01 5.8443081378936768e-01 - <_> - - 0 -1 64 2.1625359659083188e-04 - - -3.2120153307914734e-01 3.8072818517684937e-01 - <_> - - 0 -1 20 -1.2257000431418419e-02 - - 4.5801120996475220e-01 -2.8640499711036682e-01 - <_> - - 0 -1 120 -9.1817528009414673e-03 - - 5.6792247295379639e-01 -1.7843975126743317e-01 - <_> - - 0 -1 103 -1.8793119117617607e-02 - - 4.9504637718200684e-01 -2.5712665915489197e-01 - - <_> - 11 - -1.4701702594757080e+00 - - <_> - - 0 -1 3 5.8097988367080688e-02 - - -7.2848272323608398e-01 2.8767123818397522e-01 - <_> - - 0 -1 105 1.6994535923004150e-02 - - -4.4815555214881897e-01 3.1635886430740356e-01 - <_> - - 0 -1 25 -3.2376497983932495e-02 - - 4.4355374574661255e-01 -3.0353689193725586e-01 - <_> - - 0 -1 107 4.4868811964988708e-03 - - -2.5752559304237366e-01 5.3392666578292847e-01 - <_> - - 0 -1 116 1.3828460127115250e-02 - - -2.3867878317832947e-01 5.2954345941543579e-01 - <_> - - 0 -1 83 -1.0387969203293324e-02 - - 3.0829572677612305e-01 -4.8065263032913208e-01 - <_> - - 0 -1 8 -4.0221336483955383e-01 - - 1.7585287988185883e-01 -9.7357034683227539e-01 - <_> - - 0 -1 54 -3.6796718835830688e-02 - - -6.4116781949996948e-01 1.9513781368732452e-01 - <_> - - 0 -1 11 -3.4659340977668762e-02 - - 7.2359263896942139e-01 -1.9654741883277893e-01 - <_> - - 0 -1 96 -5.2678319625556469e-03 - - 3.6885800957679749e-01 -3.3221104741096497e-01 - <_> - - 0 -1 44 -2.5319552514702082e-03 - - 4.7757115960121155e-01 -2.5287047028541565e-01 - - <_> - 10 - -1.3089859485626221e+00 - - <_> - - 0 -1 18 5.2907690405845642e-03 - - -7.6158940792083740e-01 3.4965034574270248e-02 - <_> - - 0 -1 2 -8.7920054793357849e-03 - - 3.7156713008880615e-01 -3.6699345707893372e-01 - <_> - - 0 -1 114 2.7730345726013184e-02 - - -2.7396458387374878e-01 4.7486495971679688e-01 - <_> - - 0 -1 59 -5.0081420689821243e-02 - - 1.1815773695707321e-01 -9.2557746171951294e-01 - <_> - - 0 -1 50 -5.2779637277126312e-02 - - -1.8923561275005341e-01 6.2627190351486206e-01 - <_> - - 0 -1 37 -1.1504447087645531e-02 - - 6.8990832567214966e-01 -1.6575154662132263e-01 - <_> - - 0 -1 43 -4.4810341205447912e-04 - - 3.2456266880035400e-01 -2.9157385230064392e-01 - <_> - - 0 -1 57 2.3839831352233887e-02 - - 9.5953084528446198e-02 -9.4295924901962280e-01 - <_> - - 0 -1 33 -1.4092427445575595e-03 - - 5.5520117282867432e-01 -2.0291884243488312e-01 - <_> - - 0 -1 36 -1.0970374569296837e-02 - - -8.3941036462783813e-01 1.1279396712779999e-01 - - <_> - 11 - -1.1969076395034790e+00 - - <_> - - 0 -1 112 -1.7543733119964600e-02 - - -2.1052631735801697e-01 -8.2142859697341919e-01 - <_> - - 0 -1 23 -8.3853630349040031e-03 - - 2.1931529045104980e-01 -4.6866944432258606e-01 - <_> - - 0 -1 70 -1.5655077993869781e-02 - - 2.0215572416782379e-01 -5.2129101753234863e-01 - <_> - - 0 -1 56 5.7208468206226826e-04 - - -2.8226605057716370e-01 3.9611354470252991e-01 - <_> - - 0 -1 26 -4.0774211287498474e-02 - - 4.7153508663177490e-01 -1.8893110752105713e-01 - <_> - - 0 -1 106 -1.6591936349868774e-02 - - 4.1324305534362793e-01 -3.3773151040077209e-01 - <_> - - 0 -1 21 1.5747562050819397e-02 - - -2.0940901339054108e-01 5.3346717357635498e-01 - <_> - - 0 -1 80 3.4584333188831806e-03 - - -2.1836459636688232e-01 4.8452955484390259e-01 - <_> - - 0 -1 127 1.2953504920005798e-02 - - -1.8292598426342010e-01 5.0049030780792236e-01 - <_> - - 0 -1 117 -7.9962313175201416e-03 - - 5.6026089191436768e-01 -1.9084297120571136e-01 - <_> - - 0 -1 131 -9.1765663819387555e-04 - - -5.3433579206466675e-01 2.0260965824127197e-01 - - <_> - 14 - -1.5340468883514404e+00 - - <_> - - 0 -1 35 1.9439160823822021e-02 - - -7.8031086921691895e-01 -2.0000000298023224e-01 - <_> - - 0 -1 17 1.4534162357449532e-02 - - -3.5326647758483887e-01 3.7831932306289673e-01 - <_> - - 0 -1 99 -4.9835231155157089e-02 - - -8.1070756912231445e-01 1.1001287400722504e-01 - <_> - - 0 -1 109 -1.0803632438182831e-02 - - 1.5099881589412689e-01 -7.8630656003952026e-01 - <_> - - 0 -1 85 -1.1284489184617996e-02 - - 2.5855007767677307e-01 -3.5162982344627380e-01 - <_> - - 0 -1 94 -2.1756377071142197e-02 - - -7.6442658901214600e-01 1.1108461767435074e-01 - <_> - - 0 -1 39 -2.0368300378322601e-02 - - 4.4156181812286377e-01 -2.2962969541549683e-01 - <_> - - 0 -1 65 5.5380766279995441e-03 - - -2.2159671783447266e-01 4.0964427590370178e-01 - <_> - - 0 -1 121 -1.6451325267553329e-02 - - -1.5554983913898468e-01 5.4646855592727661e-01 - <_> - - 0 -1 91 -8.8985881302505732e-04 - - 3.5926350951194763e-01 -2.7782326936721802e-01 - <_> - - 0 -1 41 -3.5103228874504566e-03 - - 3.9585193991661072e-01 -2.3765103518962860e-01 - <_> - - 0 -1 133 -2.4599839001893997e-02 - - 3.5694575309753418e-01 -2.4165844917297363e-01 - <_> - - 0 -1 98 6.3818749040365219e-03 - - -1.8454258143901825e-01 4.1676977276802063e-01 - <_> - - 0 -1 115 -2.4639917537570000e-02 - - 6.5221750736236572e-01 -1.5385587513446808e-01 - - <_> - 14 - -1.4048395156860352e+00 - - <_> - - 0 -1 79 -1.1194668710231781e-02 - - -2.5688073039054871e-01 -7.5763750076293945e-01 - <_> - - 0 -1 10 -1.2587085366249084e-02 - - 4.0870183706283569e-01 -3.1488448381423950e-01 - <_> - - 0 -1 53 -3.5880759358406067e-01 - - -6.1195844411849976e-01 1.7459505796432495e-01 - <_> - - 0 -1 134 -1.6222944483160973e-02 - - 3.5100319981575012e-01 -3.0121064186096191e-01 - <_> - - 0 -1 77 -7.6989643275737762e-05 - - 2.3703606426715851e-01 -4.2212566733360291e-01 - <_> - - 0 -1 40 -9.4061775598675013e-04 - - 4.1122129559516907e-01 -1.8063741922378540e-01 - <_> - - 0 -1 27 5.8695450425148010e-03 - - -1.9567514955997467e-01 4.6317130327224731e-01 - <_> - - 0 -1 61 -5.6588794104754925e-03 - - 4.4456613063812256e-01 -1.9832246005535126e-01 - <_> - - 0 -1 82 -1.0015970095992088e-02 - - -9.6916145086288452e-01 8.8583640754222870e-02 - <_> - - 0 -1 22 -7.9611856490373611e-03 - - 3.1446850299835205e-01 -2.9137840867042542e-01 - <_> - - 0 -1 69 1.2027498334646225e-02 - - -1.7159871757030487e-01 4.9999099969863892e-01 - <_> - - 0 -1 104 -3.6678132601082325e-03 - - 3.7841594219207764e-01 -3.0834850668907166e-01 - <_> - - 0 -1 124 -1.9784808158874512e-02 - - 5.4024320840835571e-01 -1.4204865694046021e-01 - <_> - - 0 -1 76 2.7927860617637634e-02 - - -2.1247063577175140e-01 4.3218085169792175e-01 - - <_> - 11 - -1.4899781942367554e+00 - - <_> - - 0 -1 130 7.8427735716104507e-03 - - -7.4531835317611694e-01 -3.0303031206130981e-02 - <_> - - 0 -1 42 -1.4624277129769325e-02 - - -1.6841350123286247e-02 -8.5118037462234497e-01 - <_> - - 0 -1 75 -3.8077287375926971e-02 - - 2.1741871535778046e-01 -3.6631068587303162e-01 - <_> - - 0 -1 12 -1.0661857202649117e-02 - - 4.3769642710685730e-01 -2.2932954132556915e-01 - <_> - - 0 -1 88 -1.3446703553199768e-02 - - -3.8489580154418945e-01 2.5192788243293762e-01 - <_> - - 0 -1 28 -1.4191937446594238e-01 - - -8.0028277635574341e-01 9.4658434391021729e-02 - <_> - - 0 -1 123 -3.4155612229369581e-04 - - -6.2979590892791748e-01 1.1930138617753983e-01 - <_> - - 0 -1 90 1.3760997913777828e-02 - - -1.4578124880790710e-01 5.2505373954772949e-01 - <_> - - 0 -1 66 -2.6961741968989372e-02 - - 4.6245655417442322e-01 -1.6671678423881531e-01 - <_> - - 0 -1 118 6.3529079779982567e-03 - - -2.1951504051685333e-01 3.5792681574821472e-01 - <_> - - 0 -1 72 -1.2992666102945805e-02 - - 7.6828622817993164e-01 -8.5931040346622467e-02 - - <_> - 17 - -1.4984009265899658e+00 - - <_> - - 0 -1 84 6.7299734801054001e-03 - - -7.6591378450393677e-01 -2.3893804848194122e-01 - <_> - - 0 -1 19 2.6990557089447975e-03 - - -3.9991316199302673e-01 1.5220387279987335e-01 - <_> - - 0 -1 16 -2.0344853401184082e-01 - - -7.5773400068283081e-01 9.2020586133003235e-02 - <_> - - 0 -1 122 -2.0129496988374740e-04 - - -4.4318243861198425e-01 2.0615091919898987e-01 - <_> - - 0 -1 5 -2.4270072579383850e-02 - - 3.9947441220283508e-01 -2.1658866107463837e-01 - <_> - - 0 -1 93 -6.8638771772384644e-03 - - 2.1516641974449158e-01 -3.4529885649681091e-01 - <_> - - 0 -1 68 -5.8059617877006531e-03 - - 3.8044607639312744e-01 -1.6675208508968353e-01 - <_> - - 0 -1 95 -9.3506037956103683e-05 - - 2.5903701782226563e-01 -2.8573530912399292e-01 - <_> - - 0 -1 62 -1.7116001248359680e-01 - - -9.1568988561630249e-01 6.8806290626525879e-02 - <_> - - 0 -1 58 1.2468735803849995e-04 - - -2.2247949242591858e-01 3.1490680575370789e-01 - <_> - - 0 -1 24 -1.0570101439952850e-03 - - -5.3656494617462158e-01 1.4280579984188080e-01 - <_> - - 0 -1 0 -1.0864594951272011e-02 - - 5.7343137264251709e-01 -1.9628804922103882e-01 - <_> - - 0 -1 102 -6.2631990294903517e-04 - - 3.2941296696662903e-01 -2.5868213176727295e-01 - <_> - - 0 -1 60 4.5934431254863739e-02 - - -2.3765510320663452e-01 3.2077547907829285e-01 - <_> - - 0 -1 15 5.2649816498160362e-03 - - -2.7179107069969177e-01 3.0700674653053284e-01 - <_> - - 0 -1 48 -1.0489258915185928e-02 - - 5.2368879318237305e-01 -1.7894229292869568e-01 - <_> - - 0 -1 46 -2.9591415077447891e-03 - - -5.0875836610794067e-01 1.5145763754844666e-01 - - <_> - 17 - -1.1793254613876343e+00 - - <_> - - 0 -1 31 4.8567064106464386e-02 - - -7.2802960872650146e-01 -1.0924369841814041e-01 - <_> - - 0 -1 87 -9.3583632260560989e-03 - - -7.0891864597797394e-02 -7.6105058193206787e-01 - <_> - - 0 -1 1 -2.6455814018845558e-02 - - 3.2114213705062866e-01 -1.8380424380302429e-01 - <_> - - 0 -1 100 7.3388908058404922e-03 - - -1.1695016920566559e-01 4.1009449958801270e-01 - <_> - - 0 -1 132 -7.4306890368461609e-02 - - -6.3738393783569336e-01 1.2330492585897446e-01 - <_> - - 0 -1 108 -1.1535363271832466e-02 - - -7.6230877637863159e-01 7.7160604298114777e-02 - <_> - - 0 -1 4 9.8390430212020874e-02 - - -1.3311645388603210e-01 5.7595914602279663e-01 - <_> - - 0 -1 32 -1.6651481389999390e-02 - - 2.5170418620109558e-01 -2.7978977560997009e-01 - <_> - - 0 -1 63 -8.9149046689271927e-03 - - 3.8843372464179993e-01 -1.9278995692729950e-01 - <_> - - 0 -1 111 -1.7397286137565970e-04 - - 3.8313332200050354e-01 -1.8763703107833862e-01 - <_> - - 0 -1 101 -1.1212332174181938e-03 - - -4.2161688208580017e-01 1.5696237981319427e-01 - <_> - - 0 -1 97 3.7472303956747055e-02 - - -1.4634302258491516e-01 4.6759790182113647e-01 - <_> - - 0 -1 125 3.9195418357849121e-03 - - -1.4772169291973114e-01 4.4500002264976501e-01 - <_> - - 0 -1 67 -6.3096638768911362e-03 - - -8.2744646072387695e-01 8.1156894564628601e-02 - <_> - - 0 -1 13 -1.0045848786830902e-01 - - 6.1077672243118286e-01 -1.2099583446979523e-01 - <_> - - 0 -1 92 -7.4745825259014964e-04 - - 3.7484991550445557e-01 -1.9619165360927582e-01 - <_> - - 0 -1 74 3.2814389560371637e-03 - - -1.8449757993221283e-01 3.7876024842262268e-01 - - <_> - - <_> - 0 0 19 2 -1. - <_> - 0 1 19 1 2. - 0 - <_> - - <_> - 0 1 6 6 -1. - <_> - 0 4 6 3 2. - 0 - <_> - - <_> - 0 2 2 14 -1. - <_> - 0 2 1 7 2. - <_> - 1 9 1 7 2. - 0 - <_> - - <_> - 0 4 6 11 -1. - <_> - 3 4 3 11 2. - 0 - <_> - - <_> - 0 4 10 20 -1. - <_> - 5 4 5 20 2. - 0 - <_> - - <_> - 0 8 4 11 -1. - <_> - 2 8 2 11 2. - 0 - <_> - - <_> - 0 10 2 9 -1. - <_> - 1 10 1 9 2. - 0 - <_> - - <_> - 0 10 8 12 -1. - <_> - 0 10 4 6 2. - <_> - 4 16 4 6 2. - 0 - <_> - - <_> - 0 10 24 12 -1. - <_> - 0 14 24 4 2. - 0 - <_> - - <_> - 0 11 2 7 -1. - <_> - 1 11 1 7 2. - 0 - <_> - - <_> - 0 12 2 10 -1. - <_> - 1 12 1 10 2. - 0 - <_> - - <_> - 0 14 4 9 -1. - <_> - 2 14 2 9 2. - 0 - <_> - - <_> - 0 19 6 3 -1. - <_> - 3 19 3 3 2. - 0 - <_> - - <_> - 1 0 3 18 -1. - <_> - 1 9 3 9 2. - 0 - <_> - - <_> - 1 1 13 4 -1. - <_> - 1 3 13 2 2. - 0 - <_> - - <_> - 1 2 10 4 -1. - <_> - 1 2 5 2 2. - <_> - 6 4 5 2 2. - 0 - <_> - - <_> - 1 2 20 22 -1. - <_> - 1 2 10 11 2. - <_> - 11 13 10 11 2. - 0 - <_> - - <_> - 1 5 4 7 -1. - <_> - 3 5 2 7 2. - 0 - <_> - - <_> - 1 6 4 2 -1. - <_> - 3 6 2 2 2. - 0 - <_> - - <_> - 1 11 4 3 -1. - <_> - 3 11 2 3 2. - 0 - <_> - - <_> - 1 16 8 6 -1. - <_> - 1 16 4 3 2. - <_> - 5 19 4 3 2. - 0 - <_> - - <_> - 1 16 22 4 -1. - <_> - 1 18 22 2 2. - 0 - <_> - - <_> - 1 21 1 3 -1. - <_> - 1 22 1 1 2. - 0 - <_> - - <_> - 2 0 1 3 -1. - <_> - 2 1 1 1 2. - 0 - <_> - - <_> - 2 0 3 2 -1. - <_> - 2 1 3 1 2. - 0 - <_> - - <_> - 2 0 3 12 -1. - <_> - 2 6 3 6 2. - 0 - <_> - - <_> - 2 0 12 6 -1. - <_> - 2 3 12 3 2. - 0 - <_> - - <_> - 2 5 4 6 -1. - <_> - 2 5 2 3 2. - <_> - 4 8 2 3 2. - 0 - <_> - - <_> - 2 5 10 16 -1. - <_> - 2 13 10 8 2. - 0 - <_> - - <_> - 2 16 18 6 -1. - <_> - 2 18 18 2 2. - 0 - <_> - - <_> - 3 0 4 8 -1. - <_> - 3 4 4 4 2. - 0 - <_> - - <_> - 3 0 6 10 -1. - <_> - 3 5 6 5 2. - 0 - <_> - - <_> - 3 13 14 8 -1. - <_> - 3 13 7 4 2. - <_> - 10 17 7 4 2. - 0 - <_> - - <_> - 3 20 3 2 -1. - <_> - 3 21 3 1 2. - 0 - <_> - - <_> - 3 22 8 2 -1. - <_> - 3 23 8 1 2. - 0 - <_> - - <_> - 3 22 13 2 -1. - <_> - 3 23 13 1 2. - 0 - <_> - - <_> - 4 0 1 3 -1. - <_> - 4 1 1 1 2. - 0 - <_> - - <_> - 4 1 13 2 -1. - <_> - 4 2 13 1 2. - 0 - <_> - - <_> - 5 0 9 6 -1. - <_> - 5 3 9 3 2. - 0 - <_> - - <_> - 5 1 12 4 -1. - <_> - 5 3 12 2 2. - 0 - <_> - - <_> - 5 3 2 4 -1. - <_> - 5 3 1 2 2. - <_> - 6 5 1 2 2. - 0 - <_> - - <_> - 5 6 2 6 -1. - <_> - 5 9 2 3 2. - 0 - <_> - - <_> - 5 10 1 12 -1. - <_> - 5 14 1 4 2. - 0 - <_> - - <_> - 5 13 2 6 -1. - <_> - 5 13 1 3 2. - <_> - 6 16 1 3 2. - 0 - <_> - - <_> - 5 20 13 2 -1. - <_> - 5 21 13 1 2. - 0 - <_> - - <_> - 5 22 16 2 -1. - <_> - 5 23 16 1 2. - 0 - <_> - - <_> - 6 0 8 1 -1. - <_> - 10 0 4 1 2. - 0 - <_> - - <_> - 6 1 16 4 -1. - <_> - 6 3 16 2 2. - 0 - <_> - - <_> - 6 2 6 4 -1. - <_> - 6 4 6 2 2. - 0 - <_> - - <_> - 6 22 10 2 -1. - <_> - 6 23 10 1 2. - 0 - <_> - - <_> - 7 0 12 3 -1. - <_> - 11 0 4 3 2. - 0 - <_> - - <_> - 7 1 14 4 -1. - <_> - 7 3 14 2 2. - 0 - <_> - - <_> - 7 2 2 4 -1. - <_> - 7 2 1 2 2. - <_> - 8 4 1 2 2. - 0 - <_> - - <_> - 7 6 15 10 -1. - <_> - 12 6 5 10 2. - 0 - <_> - - <_> - 7 7 5 3 -1. - <_> - 7 8 5 1 2. - 0 - <_> - - <_> - 8 0 9 4 -1. - <_> - 8 2 9 2 2. - 0 - <_> - - <_> - 8 3 3 2 -1. - <_> - 8 4 3 1 2. - 0 - <_> - - <_> - 8 5 8 6 -1. - <_> - 8 8 8 3 2. - 0 - <_> - - <_> - 8 7 1 2 -1. - <_> - 8 8 1 1 2. - 0 - <_> - - <_> - 8 9 9 4 -1. - <_> - 11 9 3 4 2. - 0 - <_> - - <_> - 8 20 14 4 -1. - <_> - 8 22 14 2 2. - 0 - <_> - - <_> - 9 3 6 4 -1. - <_> - 12 3 3 4 2. - 0 - <_> - - <_> - 9 3 14 21 -1. - <_> - 16 3 7 21 2. - 0 - <_> - - <_> - 9 5 12 4 -1. - <_> - 9 7 12 2 2. - 0 - <_> - - <_> - 9 15 4 1 -1. - <_> - 11 15 2 1 2. - 0 - <_> - - <_> - 10 0 3 6 -1. - <_> - 10 3 3 3 2. - 0 - <_> - - <_> - 10 0 11 4 -1. - <_> - 10 2 11 2 2. - 0 - <_> - - <_> - 10 4 1 6 -1. - <_> - 10 7 1 3 2. - 0 - <_> - - <_> - 10 4 8 2 -1. - <_> - 14 4 4 2 2. - 0 - <_> - - <_> - 10 5 14 1 -1. - <_> - 17 5 7 1 2. - 0 - <_> - - <_> - 10 8 5 14 -1. - <_> - 10 15 5 7 2. - 0 - <_> - - <_> - 10 16 3 6 -1. - <_> - 10 18 3 2 2. - 0 - <_> - - <_> - 10 20 5 4 -1. - <_> - 10 22 5 2 2. - 0 - <_> - - <_> - 10 20 12 4 -1. - <_> - 10 22 12 2 2. - 0 - <_> - - <_> - 11 6 8 1 -1. - <_> - 15 6 4 1 2. - 0 - <_> - - <_> - 11 8 6 14 -1. - <_> - 11 15 6 7 2. - 0 - <_> - - <_> - 11 13 8 10 -1. - <_> - 11 13 4 5 2. - <_> - 15 18 4 5 2. - 0 - <_> - - <_> - 11 19 4 2 -1. - <_> - 11 20 4 1 2. - 0 - <_> - - <_> - 11 19 5 2 -1. - <_> - 11 20 5 1 2. - 0 - <_> - - <_> - 11 22 12 2 -1. - <_> - 11 22 6 1 2. - <_> - 17 23 6 1 2. - 0 - <_> - - <_> - 12 2 9 2 -1. - <_> - 12 3 9 1 2. - 0 - <_> - - <_> - 12 3 4 3 -1. - <_> - 12 4 4 1 2. - 0 - <_> - - <_> - 12 5 3 1 -1. - <_> - 13 5 1 1 2. - 0 - <_> - - <_> - 12 10 5 12 -1. - <_> - 12 16 5 6 2. - 0 - <_> - - <_> - 12 22 3 2 -1. - <_> - 12 23 3 1 2. - 0 - <_> - - <_> - 13 6 4 10 -1. - <_> - 13 11 4 5 2. - 0 - <_> - - <_> - 13 7 3 5 -1. - <_> - 14 7 1 5 2. - 0 - <_> - - <_> - 13 8 2 3 -1. - <_> - 13 9 2 1 2. - 0 - <_> - - <_> - 13 10 3 2 -1. - <_> - 14 10 1 2 2. - 0 - <_> - - <_> - 13 20 8 4 -1. - <_> - 13 22 8 2 2. - 0 - <_> - - <_> - 14 0 7 4 -1. - <_> - 14 2 7 2 2. - 0 - <_> - - <_> - 14 2 4 2 -1. - <_> - 16 2 2 2 2. - 0 - <_> - - <_> - 14 5 2 4 -1. - <_> - 15 5 1 4 2. - 0 - <_> - - <_> - 14 5 4 10 -1. - <_> - 14 10 4 5 2. - 0 - <_> - - <_> - 14 8 2 10 -1. - <_> - 14 13 2 5 2. - 0 - <_> - - <_> - 14 20 3 2 -1. - <_> - 14 21 3 1 2. - 0 - <_> - - <_> - 15 5 2 10 -1. - <_> - 15 10 2 5 2. - 0 - <_> - - <_> - 16 2 8 8 -1. - <_> - 16 6 8 4 2. - 0 - <_> - - <_> - 16 18 1 6 -1. - <_> - 16 21 1 3 2. - 0 - <_> - - <_> - 16 18 3 6 -1. - <_> - 17 18 1 6 2. - 0 - <_> - - <_> - 16 21 4 2 -1. - <_> - 16 22 4 1 2. - 0 - <_> - - <_> - 16 23 8 1 -1. - <_> - 20 23 4 1 2. - 0 - <_> - - <_> - 17 2 2 5 -1. - <_> - 18 2 1 5 2. - 0 - <_> - - <_> - 17 5 7 6 -1. - <_> - 17 8 7 3 2. - 0 - <_> - - <_> - 17 7 6 2 -1. - <_> - 20 7 3 2 2. - 0 - <_> - - <_> - 17 18 3 6 -1. - <_> - 17 21 3 3 2. - 0 - <_> - - <_> - 17 23 6 1 -1. - <_> - 19 23 2 1 2. - 0 - <_> - - <_> - 18 0 1 10 -1. - <_> - 18 5 1 5 2. - 0 - <_> - - <_> - 18 0 6 4 -1. - <_> - 21 0 3 4 2. - 0 - <_> - - <_> - 18 13 3 3 -1. - <_> - 18 14 3 1 2. - 0 - <_> - - <_> - 18 15 6 9 -1. - <_> - 21 15 3 9 2. - 0 - <_> - - <_> - 18 18 2 1 -1. - <_> - 19 18 1 1 2. - 0 - <_> - - <_> - 18 23 6 1 -1. - <_> - 20 23 2 1 2. - 0 - <_> - - <_> - 19 9 5 3 -1. - <_> - 19 10 5 1 2. - 0 - <_> - - <_> - 20 0 4 17 -1. - <_> - 22 0 2 17 2. - 0 - <_> - - <_> - 20 4 4 14 -1. - <_> - 22 4 2 14 2. - 0 - <_> - - <_> - 20 7 4 6 -1. - <_> - 22 7 2 6 2. - 0 - <_> - - <_> - 20 7 4 8 -1. - <_> - 22 7 2 8 2. - 0 - <_> - - <_> - 20 8 2 14 -1. - <_> - 20 8 1 7 2. - <_> - 21 15 1 7 2. - 0 - <_> - - <_> - 20 9 2 12 -1. - <_> - 20 9 1 6 2. - <_> - 21 15 1 6 2. - 0 - <_> - - <_> - 20 11 4 10 -1. - <_> - 22 11 2 10 2. - 0 - <_> - - <_> - 20 16 2 6 -1. - <_> - 20 18 2 2 2. - 0 - <_> - - <_> - 22 0 1 2 -1. - <_> - 22 1 1 1 2. - 0 - <_> - - <_> - 22 0 2 2 -1. - <_> - 22 0 1 1 2. - <_> - 23 1 1 1 2. - 0 - <_> - - <_> - 22 4 1 18 -1. - <_> - 22 13 1 9 2. - 0 - <_> - - <_> - 22 6 2 3 -1. - <_> - 23 6 1 3 2. - 0 - <_> - - <_> - 22 6 2 6 -1. - <_> - 23 6 1 6 2. - 0 - <_> - - <_> - 22 7 2 12 -1. - <_> - 23 7 1 12 2. - 0 - <_> - - <_> - 22 8 2 7 -1. - <_> - 23 8 1 7 2. - 0 - <_> - - <_> - 22 8 2 12 -1. - <_> - 23 8 1 12 2. - 0 - <_> - - <_> - 22 11 2 3 -1. - <_> - 23 11 1 3 2. - 0 - <_> - - <_> - 23 1 1 6 -1. - <_> - 23 4 1 3 2. - 0 - <_> - - <_> - 23 3 1 21 -1. - <_> - 23 10 1 7 2. - 0 - <_> - - <_> - 23 4 1 9 -1. - <_> - 23 7 1 3 2. - 0 - <_> - - <_> - 23 16 1 6 -1. - <_> - 23 18 1 2 2. - 0 - diff --git a/008_cascade_classifier/main.py b/008_cascade_classifier/main.py deleted file mode 100644 index 833c446..0000000 --- a/008_cascade_classifier/main.py +++ /dev/null @@ -1,53 +0,0 @@ -import cv2 as cv -import numpy as np -import os -from time import time -from windowcapture import WindowCapture -from vision import Vision - -# Change the working directory to the folder this script is in. -# Doing this because I'll be putting the files from each video in their own folder on GitHub -os.chdir(os.path.dirname(os.path.abspath(__file__))) - - -# initialize the WindowCapture class -wincap = WindowCapture('Albion Online Client') - -# load the trained model -cascade_limestone = cv.CascadeClassifier('limestone_model_final.xml') -# load an empty Vision class -vision_limestone = Vision(None) - -loop_time = time() -while(True): - - # get an updated image of the game - screenshot = wincap.get_screenshot() - - # do object detection - rectangles = cascade_limestone.detectMultiScale(screenshot) - - # draw the detection results onto the original image - detection_image = vision_limestone.draw_rectangles(screenshot, rectangles) - - # display the images - cv.imshow('Matches', detection_image) - - # debug the loop rate - print('FPS {}'.format(1 / (time() - loop_time))) - loop_time = time() - - # press 'q' with the output window focused to exit. - # press 'f' to save screenshot as a positive image, press 'd' to - # save as a negative image. - # waits 1 ms every loop to process key presses - key = cv.waitKey(1) - if key == ord('q'): - cv.destroyAllWindows() - break - elif key == ord('f'): - cv.imwrite('positive/{}.jpg'.format(loop_time), screenshot) - elif key == ord('d'): - cv.imwrite('negative/{}.jpg'.format(loop_time), screenshot) - -print('Done.') diff --git a/008_cascade_classifier/neg.txt b/008_cascade_classifier/neg.txt deleted file mode 100644 index 1b9e2be..0000000 --- a/008_cascade_classifier/neg.txt +++ /dev/null @@ -1,103 +0,0 @@ -negative/1596724705.5517466.jpg -negative/1596724746.4305139.jpg -negative/1596724758.379512.jpg -negative/1596724769.5195115.jpg -negative/1596724785.5650678.jpg -negative/1596724793.0220716.jpg -negative/1596724805.2095428.jpg -negative/1596724819.1929052.jpg -negative/1596724834.3249273.jpg -negative/1596724847.025956.jpg -negative/1596724898.553667.jpg -negative/1596724967.346105.jpg -negative/1596725038.330387.jpg -negative/1596725080.9118962.jpg -negative/1596725170.84625.jpg -negative/1596725187.592096.jpg -negative/1596725220.033521.jpg -negative/1596725264.334212.jpg -negative/1596725284.118124.jpg -negative/1596725349.903286.jpg -negative/1596725431.8746161.jpg -negative/1596725461.0213678.jpg -negative/1596725515.069354.jpg -negative/1596725582.226287.jpg -negative/1596725745.2315128.jpg -negative/1596725756.951403.jpg -negative/1596725765.261232.jpg -negative/1596725775.2572083.jpg -negative/1596725821.9031878.jpg -negative/1596725831.8041875.jpg -negative/1596725920.4239123.jpg -negative/1596725939.2339938.jpg -negative/1596725954.4924223.jpg -negative/1596726002.0213919.jpg -negative/1596726049.6788344.jpg -negative/1596726062.6242409.jpg -negative/1596726097.9022322.jpg -negative/1596726142.49426.jpg -negative/1596726155.9890828.jpg -negative/1596726182.5506525.jpg -negative/1596726217.5782723.jpg -negative/1596726234.8375776.jpg -negative/1596726246.9830372.jpg -negative/1596726292.729684.jpg -negative/1596726301.8822803.jpg -negative/1596726313.814556.jpg -negative/1596726332.8304229.jpg -negative/1596726345.7047927.jpg -negative/1596726465.2339375.jpg -negative/1596726487.8134801.jpg -negative/1596726513.185823.jpg -negative/1596726640.6113465.jpg -negative/1596726674.816247.jpg -negative/1596726689.225606.jpg -negative/1596726697.8552132.jpg -negative/1596726708.121546.jpg -negative/1596726717.2575495.jpg -negative/1596726724.903866.jpg -negative/1596726738.219499.jpg -negative/1596726750.0375574.jpg -negative/1596726757.5366132.jpg -negative/1596726822.4534585.jpg -negative/1596726831.802462.jpg -negative/1596726846.4050379.jpg -negative/1596726875.157068.jpg -negative/1596726888.3905616.jpg -negative/1596726918.442663.jpg -negative/1596726939.1568878.jpg -negative/1596726959.2844813.jpg -negative/1596726975.4137285.jpg -negative/1596727112.1766517.jpg -negative/1596727124.6547341.jpg -negative/1596727149.089741.jpg -negative/1596727157.397738.jpg -negative/1596727168.9037406.jpg -negative/1596727315.7115467.jpg -negative/1596727333.9426105.jpg -negative/1596727355.8319664.jpg -negative/1596727372.243553.jpg -negative/1596727384.199091.jpg -negative/1596727392.9071019.jpg -negative/1596727402.9000127.jpg -negative/1596727416.8854368.jpg -negative/1596727438.8985887.jpg -negative/1596727452.5146.jpg -negative/1596727470.7761557.jpg -negative/1596727485.4515324.jpg -negative/1596727494.702687.jpg -negative/1596727506.0782263.jpg -negative/1596727510.119744.jpg -negative/1596727522.5037456.jpg -negative/1596727529.4377453.jpg -negative/1596727538.2637434.jpg -negative/1596727547.951745.jpg -negative/1596727558.6709976.jpg -negative/1596727568.9909968.jpg -negative/1596727581.266752.jpg -negative/1596727595.2124581.jpg -negative/1596727609.8064606.jpg -negative/1596727646.9046378.jpg -negative/1596727657.9977558.jpg -negative/1596727863.9691815.jpg -negative/1597344765.5637424.jpg diff --git a/008_cascade_classifier/negative/1596724705.5517466.jpg b/008_cascade_classifier/negative/1596724705.5517466.jpg deleted file mode 100644 index e26f738..0000000 Binary files a/008_cascade_classifier/negative/1596724705.5517466.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596724746.4305139.jpg b/008_cascade_classifier/negative/1596724746.4305139.jpg deleted file mode 100644 index 77da23f..0000000 Binary files a/008_cascade_classifier/negative/1596724746.4305139.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596724758.379512.jpg b/008_cascade_classifier/negative/1596724758.379512.jpg deleted file mode 100644 index aeae205..0000000 Binary files a/008_cascade_classifier/negative/1596724758.379512.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596724769.5195115.jpg b/008_cascade_classifier/negative/1596724769.5195115.jpg deleted file mode 100644 index d1fcec4..0000000 Binary files a/008_cascade_classifier/negative/1596724769.5195115.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596724785.5650678.jpg b/008_cascade_classifier/negative/1596724785.5650678.jpg deleted file mode 100644 index 31ae241..0000000 Binary files a/008_cascade_classifier/negative/1596724785.5650678.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596724793.0220716.jpg b/008_cascade_classifier/negative/1596724793.0220716.jpg deleted file mode 100644 index 239eb8c..0000000 Binary files a/008_cascade_classifier/negative/1596724793.0220716.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596724805.2095428.jpg b/008_cascade_classifier/negative/1596724805.2095428.jpg deleted file mode 100644 index ed44be7..0000000 Binary files a/008_cascade_classifier/negative/1596724805.2095428.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596724819.1929052.jpg b/008_cascade_classifier/negative/1596724819.1929052.jpg deleted file mode 100644 index 5fc0444..0000000 Binary files a/008_cascade_classifier/negative/1596724819.1929052.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596724834.3249273.jpg b/008_cascade_classifier/negative/1596724834.3249273.jpg deleted file mode 100644 index 07a5145..0000000 Binary files a/008_cascade_classifier/negative/1596724834.3249273.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596724847.025956.jpg b/008_cascade_classifier/negative/1596724847.025956.jpg deleted file mode 100644 index cbccaa2..0000000 Binary files a/008_cascade_classifier/negative/1596724847.025956.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596724898.553667.jpg b/008_cascade_classifier/negative/1596724898.553667.jpg deleted file mode 100644 index ca78f77..0000000 Binary files a/008_cascade_classifier/negative/1596724898.553667.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596724967.346105.jpg b/008_cascade_classifier/negative/1596724967.346105.jpg deleted file mode 100644 index 064a057..0000000 Binary files a/008_cascade_classifier/negative/1596724967.346105.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725038.330387.jpg b/008_cascade_classifier/negative/1596725038.330387.jpg deleted file mode 100644 index d1a511e..0000000 Binary files a/008_cascade_classifier/negative/1596725038.330387.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725080.9118962.jpg b/008_cascade_classifier/negative/1596725080.9118962.jpg deleted file mode 100644 index 3caf3b9..0000000 Binary files a/008_cascade_classifier/negative/1596725080.9118962.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725170.84625.jpg b/008_cascade_classifier/negative/1596725170.84625.jpg deleted file mode 100644 index 9d62b60..0000000 Binary files a/008_cascade_classifier/negative/1596725170.84625.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725187.592096.jpg b/008_cascade_classifier/negative/1596725187.592096.jpg deleted file mode 100644 index a08a3ab..0000000 Binary files a/008_cascade_classifier/negative/1596725187.592096.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725220.033521.jpg b/008_cascade_classifier/negative/1596725220.033521.jpg deleted file mode 100644 index c83e6ea..0000000 Binary files a/008_cascade_classifier/negative/1596725220.033521.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725264.334212.jpg b/008_cascade_classifier/negative/1596725264.334212.jpg deleted file mode 100644 index 2ebe866..0000000 Binary files a/008_cascade_classifier/negative/1596725264.334212.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725284.118124.jpg b/008_cascade_classifier/negative/1596725284.118124.jpg deleted file mode 100644 index d57189f..0000000 Binary files a/008_cascade_classifier/negative/1596725284.118124.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725349.903286.jpg b/008_cascade_classifier/negative/1596725349.903286.jpg deleted file mode 100644 index abb4385..0000000 Binary files a/008_cascade_classifier/negative/1596725349.903286.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725431.8746161.jpg b/008_cascade_classifier/negative/1596725431.8746161.jpg deleted file mode 100644 index b2f4712..0000000 Binary files a/008_cascade_classifier/negative/1596725431.8746161.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725461.0213678.jpg b/008_cascade_classifier/negative/1596725461.0213678.jpg deleted file mode 100644 index 8b800c1..0000000 Binary files a/008_cascade_classifier/negative/1596725461.0213678.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725515.069354.jpg b/008_cascade_classifier/negative/1596725515.069354.jpg deleted file mode 100644 index cb100db..0000000 Binary files a/008_cascade_classifier/negative/1596725515.069354.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725582.226287.jpg b/008_cascade_classifier/negative/1596725582.226287.jpg deleted file mode 100644 index 15b271c..0000000 Binary files a/008_cascade_classifier/negative/1596725582.226287.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725745.2315128.jpg b/008_cascade_classifier/negative/1596725745.2315128.jpg deleted file mode 100644 index 2905156..0000000 Binary files a/008_cascade_classifier/negative/1596725745.2315128.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725756.951403.jpg b/008_cascade_classifier/negative/1596725756.951403.jpg deleted file mode 100644 index 4be535a..0000000 Binary files a/008_cascade_classifier/negative/1596725756.951403.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725765.261232.jpg b/008_cascade_classifier/negative/1596725765.261232.jpg deleted file mode 100644 index c3d3a83..0000000 Binary files a/008_cascade_classifier/negative/1596725765.261232.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725775.2572083.jpg b/008_cascade_classifier/negative/1596725775.2572083.jpg deleted file mode 100644 index 09ebe80..0000000 Binary files a/008_cascade_classifier/negative/1596725775.2572083.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725821.9031878.jpg b/008_cascade_classifier/negative/1596725821.9031878.jpg deleted file mode 100644 index 4375cb2..0000000 Binary files a/008_cascade_classifier/negative/1596725821.9031878.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725831.8041875.jpg b/008_cascade_classifier/negative/1596725831.8041875.jpg deleted file mode 100644 index 986c903..0000000 Binary files a/008_cascade_classifier/negative/1596725831.8041875.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725920.4239123.jpg b/008_cascade_classifier/negative/1596725920.4239123.jpg deleted file mode 100644 index e1ed54c..0000000 Binary files a/008_cascade_classifier/negative/1596725920.4239123.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725939.2339938.jpg b/008_cascade_classifier/negative/1596725939.2339938.jpg deleted file mode 100644 index 5745988..0000000 Binary files a/008_cascade_classifier/negative/1596725939.2339938.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596725954.4924223.jpg b/008_cascade_classifier/negative/1596725954.4924223.jpg deleted file mode 100644 index a57b4d6..0000000 Binary files a/008_cascade_classifier/negative/1596725954.4924223.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726002.0213919.jpg b/008_cascade_classifier/negative/1596726002.0213919.jpg deleted file mode 100644 index 4decabf..0000000 Binary files a/008_cascade_classifier/negative/1596726002.0213919.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726049.6788344.jpg b/008_cascade_classifier/negative/1596726049.6788344.jpg deleted file mode 100644 index d05554b..0000000 Binary files a/008_cascade_classifier/negative/1596726049.6788344.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726062.6242409.jpg b/008_cascade_classifier/negative/1596726062.6242409.jpg deleted file mode 100644 index 773ba25..0000000 Binary files a/008_cascade_classifier/negative/1596726062.6242409.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726097.9022322.jpg b/008_cascade_classifier/negative/1596726097.9022322.jpg deleted file mode 100644 index ad4fa77..0000000 Binary files a/008_cascade_classifier/negative/1596726097.9022322.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726142.49426.jpg b/008_cascade_classifier/negative/1596726142.49426.jpg deleted file mode 100644 index 29031d9..0000000 Binary files a/008_cascade_classifier/negative/1596726142.49426.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726155.9890828.jpg b/008_cascade_classifier/negative/1596726155.9890828.jpg deleted file mode 100644 index 9f1bfca..0000000 Binary files a/008_cascade_classifier/negative/1596726155.9890828.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726182.5506525.jpg b/008_cascade_classifier/negative/1596726182.5506525.jpg deleted file mode 100644 index 9e5e64b..0000000 Binary files a/008_cascade_classifier/negative/1596726182.5506525.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726217.5782723.jpg b/008_cascade_classifier/negative/1596726217.5782723.jpg deleted file mode 100644 index b66a034..0000000 Binary files a/008_cascade_classifier/negative/1596726217.5782723.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726234.8375776.jpg b/008_cascade_classifier/negative/1596726234.8375776.jpg deleted file mode 100644 index 3b0b013..0000000 Binary files a/008_cascade_classifier/negative/1596726234.8375776.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726246.9830372.jpg b/008_cascade_classifier/negative/1596726246.9830372.jpg deleted file mode 100644 index ca129ad..0000000 Binary files a/008_cascade_classifier/negative/1596726246.9830372.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726292.729684.jpg b/008_cascade_classifier/negative/1596726292.729684.jpg deleted file mode 100644 index 31fd8b3..0000000 Binary files a/008_cascade_classifier/negative/1596726292.729684.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726301.8822803.jpg b/008_cascade_classifier/negative/1596726301.8822803.jpg deleted file mode 100644 index 25835d5..0000000 Binary files a/008_cascade_classifier/negative/1596726301.8822803.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726313.814556.jpg b/008_cascade_classifier/negative/1596726313.814556.jpg deleted file mode 100644 index 644d544..0000000 Binary files a/008_cascade_classifier/negative/1596726313.814556.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726332.8304229.jpg b/008_cascade_classifier/negative/1596726332.8304229.jpg deleted file mode 100644 index 13eb4da..0000000 Binary files a/008_cascade_classifier/negative/1596726332.8304229.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726345.7047927.jpg b/008_cascade_classifier/negative/1596726345.7047927.jpg deleted file mode 100644 index 1119ab1..0000000 Binary files a/008_cascade_classifier/negative/1596726345.7047927.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726465.2339375.jpg b/008_cascade_classifier/negative/1596726465.2339375.jpg deleted file mode 100644 index cc7015f..0000000 Binary files a/008_cascade_classifier/negative/1596726465.2339375.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726487.8134801.jpg b/008_cascade_classifier/negative/1596726487.8134801.jpg deleted file mode 100644 index c5d094c..0000000 Binary files a/008_cascade_classifier/negative/1596726487.8134801.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726513.185823.jpg b/008_cascade_classifier/negative/1596726513.185823.jpg deleted file mode 100644 index 6ee4150..0000000 Binary files a/008_cascade_classifier/negative/1596726513.185823.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726640.6113465.jpg b/008_cascade_classifier/negative/1596726640.6113465.jpg deleted file mode 100644 index 5b3a1d4..0000000 Binary files a/008_cascade_classifier/negative/1596726640.6113465.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726674.816247.jpg b/008_cascade_classifier/negative/1596726674.816247.jpg deleted file mode 100644 index da0e5f2..0000000 Binary files a/008_cascade_classifier/negative/1596726674.816247.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726689.225606.jpg b/008_cascade_classifier/negative/1596726689.225606.jpg deleted file mode 100644 index dd5a390..0000000 Binary files a/008_cascade_classifier/negative/1596726689.225606.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726697.8552132.jpg b/008_cascade_classifier/negative/1596726697.8552132.jpg deleted file mode 100644 index 8b34ce2..0000000 Binary files a/008_cascade_classifier/negative/1596726697.8552132.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726708.121546.jpg b/008_cascade_classifier/negative/1596726708.121546.jpg deleted file mode 100644 index 03af6e2..0000000 Binary files a/008_cascade_classifier/negative/1596726708.121546.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726717.2575495.jpg b/008_cascade_classifier/negative/1596726717.2575495.jpg deleted file mode 100644 index 647a8e1..0000000 Binary files a/008_cascade_classifier/negative/1596726717.2575495.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726724.903866.jpg b/008_cascade_classifier/negative/1596726724.903866.jpg deleted file mode 100644 index ddff487..0000000 Binary files a/008_cascade_classifier/negative/1596726724.903866.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726738.219499.jpg b/008_cascade_classifier/negative/1596726738.219499.jpg deleted file mode 100644 index 0f9a4fe..0000000 Binary files a/008_cascade_classifier/negative/1596726738.219499.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726750.0375574.jpg b/008_cascade_classifier/negative/1596726750.0375574.jpg deleted file mode 100644 index e012941..0000000 Binary files a/008_cascade_classifier/negative/1596726750.0375574.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726757.5366132.jpg b/008_cascade_classifier/negative/1596726757.5366132.jpg deleted file mode 100644 index 3139d8c..0000000 Binary files a/008_cascade_classifier/negative/1596726757.5366132.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726822.4534585.jpg b/008_cascade_classifier/negative/1596726822.4534585.jpg deleted file mode 100644 index 5619ac3..0000000 Binary files a/008_cascade_classifier/negative/1596726822.4534585.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726831.802462.jpg b/008_cascade_classifier/negative/1596726831.802462.jpg deleted file mode 100644 index f4460f8..0000000 Binary files a/008_cascade_classifier/negative/1596726831.802462.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726846.4050379.jpg b/008_cascade_classifier/negative/1596726846.4050379.jpg deleted file mode 100644 index 89804c3..0000000 Binary files a/008_cascade_classifier/negative/1596726846.4050379.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726875.157068.jpg b/008_cascade_classifier/negative/1596726875.157068.jpg deleted file mode 100644 index 865ccbb..0000000 Binary files a/008_cascade_classifier/negative/1596726875.157068.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726888.3905616.jpg b/008_cascade_classifier/negative/1596726888.3905616.jpg deleted file mode 100644 index ba22e26..0000000 Binary files a/008_cascade_classifier/negative/1596726888.3905616.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726918.442663.jpg b/008_cascade_classifier/negative/1596726918.442663.jpg deleted file mode 100644 index a5aeff2..0000000 Binary files a/008_cascade_classifier/negative/1596726918.442663.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726939.1568878.jpg b/008_cascade_classifier/negative/1596726939.1568878.jpg deleted file mode 100644 index bef7dda..0000000 Binary files a/008_cascade_classifier/negative/1596726939.1568878.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726959.2844813.jpg b/008_cascade_classifier/negative/1596726959.2844813.jpg deleted file mode 100644 index 6fb984c..0000000 Binary files a/008_cascade_classifier/negative/1596726959.2844813.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596726975.4137285.jpg b/008_cascade_classifier/negative/1596726975.4137285.jpg deleted file mode 100644 index f2b123e..0000000 Binary files a/008_cascade_classifier/negative/1596726975.4137285.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727112.1766517.jpg b/008_cascade_classifier/negative/1596727112.1766517.jpg deleted file mode 100644 index 9674575..0000000 Binary files a/008_cascade_classifier/negative/1596727112.1766517.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727124.6547341.jpg b/008_cascade_classifier/negative/1596727124.6547341.jpg deleted file mode 100644 index 221fb14..0000000 Binary files a/008_cascade_classifier/negative/1596727124.6547341.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727149.089741.jpg b/008_cascade_classifier/negative/1596727149.089741.jpg deleted file mode 100644 index 8812a90..0000000 Binary files a/008_cascade_classifier/negative/1596727149.089741.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727157.397738.jpg b/008_cascade_classifier/negative/1596727157.397738.jpg deleted file mode 100644 index 1ca3c49..0000000 Binary files a/008_cascade_classifier/negative/1596727157.397738.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727168.9037406.jpg b/008_cascade_classifier/negative/1596727168.9037406.jpg deleted file mode 100644 index 80c2177..0000000 Binary files a/008_cascade_classifier/negative/1596727168.9037406.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727315.7115467.jpg b/008_cascade_classifier/negative/1596727315.7115467.jpg deleted file mode 100644 index a8b90c2..0000000 Binary files a/008_cascade_classifier/negative/1596727315.7115467.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727333.9426105.jpg b/008_cascade_classifier/negative/1596727333.9426105.jpg deleted file mode 100644 index b26273b..0000000 Binary files a/008_cascade_classifier/negative/1596727333.9426105.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727355.8319664.jpg b/008_cascade_classifier/negative/1596727355.8319664.jpg deleted file mode 100644 index 2c80412..0000000 Binary files a/008_cascade_classifier/negative/1596727355.8319664.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727372.243553.jpg b/008_cascade_classifier/negative/1596727372.243553.jpg deleted file mode 100644 index 6243700..0000000 Binary files a/008_cascade_classifier/negative/1596727372.243553.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727384.199091.jpg b/008_cascade_classifier/negative/1596727384.199091.jpg deleted file mode 100644 index 74f1676..0000000 Binary files a/008_cascade_classifier/negative/1596727384.199091.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727392.9071019.jpg b/008_cascade_classifier/negative/1596727392.9071019.jpg deleted file mode 100644 index 36e2b9d..0000000 Binary files a/008_cascade_classifier/negative/1596727392.9071019.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727402.9000127.jpg b/008_cascade_classifier/negative/1596727402.9000127.jpg deleted file mode 100644 index a33b1f7..0000000 Binary files a/008_cascade_classifier/negative/1596727402.9000127.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727416.8854368.jpg b/008_cascade_classifier/negative/1596727416.8854368.jpg deleted file mode 100644 index 63ed3d3..0000000 Binary files a/008_cascade_classifier/negative/1596727416.8854368.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727438.8985887.jpg b/008_cascade_classifier/negative/1596727438.8985887.jpg deleted file mode 100644 index 33d3a94..0000000 Binary files a/008_cascade_classifier/negative/1596727438.8985887.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727452.5146.jpg b/008_cascade_classifier/negative/1596727452.5146.jpg deleted file mode 100644 index 5f792c4..0000000 Binary files a/008_cascade_classifier/negative/1596727452.5146.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727470.7761557.jpg b/008_cascade_classifier/negative/1596727470.7761557.jpg deleted file mode 100644 index 52536b8..0000000 Binary files a/008_cascade_classifier/negative/1596727470.7761557.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727485.4515324.jpg b/008_cascade_classifier/negative/1596727485.4515324.jpg deleted file mode 100644 index 1cfb516..0000000 Binary files a/008_cascade_classifier/negative/1596727485.4515324.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727494.702687.jpg b/008_cascade_classifier/negative/1596727494.702687.jpg deleted file mode 100644 index ecb7d68..0000000 Binary files a/008_cascade_classifier/negative/1596727494.702687.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727506.0782263.jpg b/008_cascade_classifier/negative/1596727506.0782263.jpg deleted file mode 100644 index cedf4d5..0000000 Binary files a/008_cascade_classifier/negative/1596727506.0782263.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727510.119744.jpg b/008_cascade_classifier/negative/1596727510.119744.jpg deleted file mode 100644 index 4181a96..0000000 Binary files a/008_cascade_classifier/negative/1596727510.119744.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727522.5037456.jpg b/008_cascade_classifier/negative/1596727522.5037456.jpg deleted file mode 100644 index a965efb..0000000 Binary files a/008_cascade_classifier/negative/1596727522.5037456.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727529.4377453.jpg b/008_cascade_classifier/negative/1596727529.4377453.jpg deleted file mode 100644 index a80721e..0000000 Binary files a/008_cascade_classifier/negative/1596727529.4377453.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727538.2637434.jpg b/008_cascade_classifier/negative/1596727538.2637434.jpg deleted file mode 100644 index ec718bc..0000000 Binary files a/008_cascade_classifier/negative/1596727538.2637434.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727547.951745.jpg b/008_cascade_classifier/negative/1596727547.951745.jpg deleted file mode 100644 index ab461a5..0000000 Binary files a/008_cascade_classifier/negative/1596727547.951745.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727558.6709976.jpg b/008_cascade_classifier/negative/1596727558.6709976.jpg deleted file mode 100644 index c807a42..0000000 Binary files a/008_cascade_classifier/negative/1596727558.6709976.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727568.9909968.jpg b/008_cascade_classifier/negative/1596727568.9909968.jpg deleted file mode 100644 index 7a87806..0000000 Binary files a/008_cascade_classifier/negative/1596727568.9909968.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727581.266752.jpg b/008_cascade_classifier/negative/1596727581.266752.jpg deleted file mode 100644 index a681da5..0000000 Binary files a/008_cascade_classifier/negative/1596727581.266752.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727595.2124581.jpg b/008_cascade_classifier/negative/1596727595.2124581.jpg deleted file mode 100644 index 80b0669..0000000 Binary files a/008_cascade_classifier/negative/1596727595.2124581.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727609.8064606.jpg b/008_cascade_classifier/negative/1596727609.8064606.jpg deleted file mode 100644 index 2f613d2..0000000 Binary files a/008_cascade_classifier/negative/1596727609.8064606.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727646.9046378.jpg b/008_cascade_classifier/negative/1596727646.9046378.jpg deleted file mode 100644 index 5158393..0000000 Binary files a/008_cascade_classifier/negative/1596727646.9046378.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727657.9977558.jpg b/008_cascade_classifier/negative/1596727657.9977558.jpg deleted file mode 100644 index a56c30f..0000000 Binary files a/008_cascade_classifier/negative/1596727657.9977558.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1596727863.9691815.jpg b/008_cascade_classifier/negative/1596727863.9691815.jpg deleted file mode 100644 index 166abfc..0000000 Binary files a/008_cascade_classifier/negative/1596727863.9691815.jpg and /dev/null differ diff --git a/008_cascade_classifier/negative/1597344765.5637424.jpg b/008_cascade_classifier/negative/1597344765.5637424.jpg deleted file mode 100644 index f1d0e84..0000000 Binary files a/008_cascade_classifier/negative/1597344765.5637424.jpg and /dev/null differ diff --git a/008_cascade_classifier/pos.txt b/008_cascade_classifier/pos.txt deleted file mode 100644 index e7da45f..0000000 --- a/008_cascade_classifier/pos.txt +++ /dev/null @@ -1,105 +0,0 @@ -positive/1596724594.7233658.jpg 2 899 378 127 120 923 1 91 48 -positive/1596724628.6711628.jpg 2 890 497 144 133 929 44 94 68 -positive/1596724646.3438368.jpg 2 581 546 150 150 720 67 91 72 -positive/1596724654.9572363.jpg 3 920 129 100 86 965 477 126 130 1074 608 133 133 -positive/1596724663.8320026.jpg 5 868 12 88 69 890 288 112 101 984 390 105 97 1173 647 108 124 1173 647 108 124 -positive/1596724674.324035.jpg 3 808 153 98 89 889 242 91 80 1023 446 104 102 -positive/1596724688.3025408.jpg 3 997 42 94 72 1081 107 87 69 1250 268 67 80 -positive/1596724716.7686796.jpg 2 446 498 151 123 357 595 145 120 -positive/1596724725.0338614.jpg 2 451 428 143 116 370 506 124 132 -positive/1596724731.2288644.jpg 3 492 190 115 88 423 238 104 96 1043 506 121 89 -positive/1596724862.2278488.jpg 4 341 82 99 84 291 216 121 85 656 34 86 72 939 248 100 94 -positive/1596724877.1428533.jpg 4 550 303 106 98 514 484 136 116 897 225 113 95 1290 521 131 140 -positive/1596724932.9526978.jpg 3 256 240 117 64 439 621 136 118 1439 416 133 93 -positive/1596724981.3780077.jpg 2 553 167 97 84 650 168 101 83 -positive/1596725001.129405.jpg 1 234 283 142 79 -positive/1596725013.2133152.jpg 2 537 130 116 85 574 213 104 88 -positive/1596725024.112249.jpg 3 368 426 138 103 816 514 154 145 894 659 133 155 -positive/1596725056.3050916.jpg 2 585 40 98 63 970 95 46 56 -positive/1596725091.287897.jpg 1 822 99 66 79 -positive/1596725112.003226.jpg 2 616 69 93 77 1153 346 102 107 -positive/1596725119.7605214.jpg 2 359 364 136 98 957 179 111 94 -positive/1596725133.894812.jpg 1 599 87 107 82 -positive/1596725145.224427.jpg 1 105 346 139 90 -positive/1596725160.9995346.jpg 3 408 143 126 91 616 100 99 80 720 94 91 70 -positive/1596725207.3700407.jpg 3 368 157 107 80 970 136 97 85 511 151 87 87 -positive/1596725236.9265978.jpg 2 953 133 90 83 1141 75 90 65 -positive/1596725319.6801374.jpg 3 303 422 123 113 505 153 91 63 1238 46 107 61 -positive/1596725331.6399002.jpg 2 1153 181 104 91 889 652 158 150 -positive/1596725365.0860987.jpg 1 915 471 114 90 -positive/1596725448.8122423.jpg 1 422 314 103 98 -positive/1596725482.437604.jpg 2 608 517 147 124 649 652 146 141 -positive/1596725504.2595575.jpg 1 492 198 109 88 -positive/1596725531.0743005.jpg 1 1109 149 80 90 -positive/1596725547.959211.jpg 5 351 100 110 76 395 176 93 87 86 309 137 96 621 560 143 127 1091 1 96 55 -positive/1596725557.5062091.jpg 4 492 278 113 101 202 574 149 113 546 387 107 103 1325 137 88 76 -positive/1596725570.5192885.jpg 1 751 491 118 108 -positive/1596725594.7959447.jpg 2 1242 492 137 119 849 588 127 148 -positive/1596725612.3280354.jpg 2 957 481 125 130 1070 604 129 145 -positive/1596725623.4045992.jpg 4 861 277 110 105 952 366 112 116 1124 625 118 134 839 5 102 76 -positive/1596725636.845524.jpg 2 939 118 101 77 904 657 149 159 -positive/1596725656.2454305.jpg 3 303 423 128 115 1288 579 122 126 512 0 95 60 -positive/1596725673.0234246.jpg 2 350 371 108 107 1312 519 119 130 -positive/1596725683.22975.jpg 3 917 144 104 95 952 503 141 142 1073 625 137 164 -positive/1596725691.7093291.jpg 3 864 141 102 86 951 214 94 98 1104 417 98 106 -positive/1596725703.587616.jpg 2 1240 176 114 97 1347 258 112 98 -positive/1596725715.009877.jpg 2 366 504 149 112 273 581 136 139 -positive/1596725732.5384405.jpg 3 816 552 113 93 286 218 122 84 196 269 121 99 -positive/1596725787.8061755.jpg 2 1038 115 96 87 1128 189 96 86 -positive/1596725809.2862072.jpg 4 706 61 91 82 967 477 115 124 1118 427 121 82 1066 12 77 74 -positive/1596725850.87147.jpg 4 445 153 97 90 852 92 90 87 823 595 125 99 629 652 134 126 -positive/1596725863.4828084.jpg 3 168 463 130 118 399 176 102 64 1164 61 114 69 -positive/1596725874.1538086.jpg 3 27 256 121 67 142 655 142 111 1214 437 149 97 -positive/1596725886.3860302.jpg 4 375 45 90 74 720 22 86 68 1307 428 154 137 538 688 181 117 -positive/1596725967.164775.jpg 2 500 634 132 150 1470 499 129 152 -positive/1596725983.5232484.jpg 1 210 404 119 121 -positive/1596726015.8447444.jpg 1 997 512 147 113 -positive/1596726023.5497692.jpg 2 945 512 134 110 1112 293 129 89 -positive/1596726034.82377.jpg 3 642 500 116 95 64 239 124 88 156 182 127 86 -positive/1596726085.3942282.jpg 1 1095 312 81 91 -positive/1596726109.9420922.jpg 2 873 562 151 115 504 662 137 129 -positive/1596726116.478095.jpg 3 751 563 127 116 865 494 137 97 1077 468 146 114 -positive/1596726192.4596026.jpg 2 173 356 142 113 256 226 144 103 -positive/1596726416.5257359.jpg 1 1307 229 76 84 -positive/1596726430.6089704.jpg 2 361 528 133 121 454 436 140 132 -positive/1596726442.6422012.jpg 1 214 297 126 63 -positive/1596726453.5657032.jpg 2 1076 340 116 93 899 570 125 116 -positive/1596726474.629971.jpg 2 612 480 144 139 673 619 133 139 -positive/1596726526.9982874.jpg 2 791 60 95 81 235 35 103 74 -positive/1596726536.5520952.jpg 4 526 134 103 94 502 284 120 96 842 78 103 87 1181 312 119 111 -positive/1596726552.1565392.jpg 3 638 445 120 128 1023 355 118 119 618 677 142 139 -positive/1596726566.051003.jpg 2 529 576 127 150 673 73 98 84 -positive/1596726579.2008128.jpg 3 921 128 98 92 955 471 137 142 1068 598 140 158 -positive/1596726626.1602285.jpg 2 495 426 141 119 410 506 139 133 -positive/1596726654.1444535.jpg 3 522 287 126 107 445 347 118 116 1137 665 129 118 -positive/1596726662.3032484.jpg 3 627 116 107 79 564 162 103 84 1165 390 107 86 -positive/1596726774.8108573.jpg 4 343 332 116 97 1236 59 97 87 1246 157 99 94 1502 250 89 100 -positive/1596726788.6514382.jpg 3 889 60 99 74 879 153 78 91 1094 247 101 91 -positive/1596726803.447783.jpg 1 203 198 129 80 -positive/1596726860.5150664.jpg 1 1191 263 102 106 -positive/1596726901.542625.jpg 1 994 174 87 91 -positive/1596726926.4286637.jpg 1 993 10 100 90 -positive/1596726947.2468882.jpg 1 1000 106 94 94 -positive/1596726987.308508.jpg 1 1096 302 117 110 -positive/1596727004.7786236.jpg 5 108 145 128 83 133 231 109 89 925 25 90 74 1481 183 78 95 297 664 160 135 -positive/1596727013.9736161.jpg 5 512 148 110 96 560 244 106 95 264 388 142 118 884 665 132 155 1279 49 87 64 -positive/1596727037.3830843.jpg 1 816 596 137 150 -positive/1596727055.9510057.jpg 4 552 233 113 107 528 409 133 117 895 177 105 86 1274 444 118 131 -positive/1596727069.049649.jpg 2 945 107 89 84 884 621 169 176 -positive/1596727079.762559.jpg 3 904 137 119 96 943 489 146 153 1059 617 145 178 -positive/1596727099.340134.jpg 2 474 592 143 143 558 500 148 147 -positive/1596727137.8729897.jpg 3 881 29 92 73 835 68 89 78 1404 263 104 65 -positive/1596727178.5962224.jpg 2 1243 245 127 85 1111 447 122 106 -positive/1596727189.9316707.jpg 2 145 302 132 53 888 66 53 75 -positive/1596727203.982804.jpg 4 170 285 144 105 66 352 145 104 712 668 127 113 1441 308 133 76 -positive/1596727229.6601653.jpg 3 853 143 101 91 931 222 95 95 1086 427 98 108 -positive/1596727241.441173.jpg 2 966 54 94 76 941 525 149 146 -positive/1596727249.654685.jpg 2 778 8 92 76 680 441 136 131 -positive/1596727259.3076847.jpg 1 1161 365 136 120 -positive/1596727272.6673214.jpg 2 1141 153 101 72 1016 189 102 93 -positive/1596727293.8035266.jpg 4 666 93 105 90 1047 40 78 86 1088 483 132 105 920 538 130 123 -positive/1596727755.6122937.jpg 3 973 258 132 112 424 469 150 143 361 226 122 89 -positive/1596727802.904577.jpg 2 958 373 137 100 752 612 128 125 -positive/1596727822.1659546.jpg 4 666 66 93 93 329 123 115 90 290 261 122 106 963 287 101 118 -positive/1596727837.7473912.jpg 2 889 78 112 84 840 580 148 162 -positive/1596727848.3987978.jpg 3 946 27 93 80 982 302 119 122 1089 402 116 129 diff --git a/008_cascade_classifier/pos.vec b/008_cascade_classifier/pos.vec deleted file mode 100644 index fff8cdc..0000000 Binary files a/008_cascade_classifier/pos.vec and /dev/null differ diff --git a/008_cascade_classifier/positive/1596724594.7233658.jpg b/008_cascade_classifier/positive/1596724594.7233658.jpg deleted file mode 100644 index fb8a401..0000000 Binary files a/008_cascade_classifier/positive/1596724594.7233658.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596724628.6711628.jpg b/008_cascade_classifier/positive/1596724628.6711628.jpg deleted file mode 100644 index b064217..0000000 Binary files a/008_cascade_classifier/positive/1596724628.6711628.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596724646.3438368.jpg b/008_cascade_classifier/positive/1596724646.3438368.jpg deleted file mode 100644 index 8030f91..0000000 Binary files a/008_cascade_classifier/positive/1596724646.3438368.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596724654.9572363.jpg b/008_cascade_classifier/positive/1596724654.9572363.jpg deleted file mode 100644 index 1cce09e..0000000 Binary files a/008_cascade_classifier/positive/1596724654.9572363.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596724663.8320026.jpg b/008_cascade_classifier/positive/1596724663.8320026.jpg deleted file mode 100644 index def1918..0000000 Binary files a/008_cascade_classifier/positive/1596724663.8320026.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596724674.324035.jpg b/008_cascade_classifier/positive/1596724674.324035.jpg deleted file mode 100644 index c0c8c0a..0000000 Binary files a/008_cascade_classifier/positive/1596724674.324035.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596724688.3025408.jpg b/008_cascade_classifier/positive/1596724688.3025408.jpg deleted file mode 100644 index 52199da..0000000 Binary files a/008_cascade_classifier/positive/1596724688.3025408.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596724716.7686796.jpg b/008_cascade_classifier/positive/1596724716.7686796.jpg deleted file mode 100644 index 1bfe4ba..0000000 Binary files a/008_cascade_classifier/positive/1596724716.7686796.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596724725.0338614.jpg b/008_cascade_classifier/positive/1596724725.0338614.jpg deleted file mode 100644 index f88ddbc..0000000 Binary files a/008_cascade_classifier/positive/1596724725.0338614.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596724731.2288644.jpg b/008_cascade_classifier/positive/1596724731.2288644.jpg deleted file mode 100644 index 6b8e1ea..0000000 Binary files a/008_cascade_classifier/positive/1596724731.2288644.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596724862.2278488.jpg b/008_cascade_classifier/positive/1596724862.2278488.jpg deleted file mode 100644 index 31930fe..0000000 Binary files a/008_cascade_classifier/positive/1596724862.2278488.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596724877.1428533.jpg b/008_cascade_classifier/positive/1596724877.1428533.jpg deleted file mode 100644 index 5385ac7..0000000 Binary files a/008_cascade_classifier/positive/1596724877.1428533.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596724932.9526978.jpg b/008_cascade_classifier/positive/1596724932.9526978.jpg deleted file mode 100644 index 12464c4..0000000 Binary files a/008_cascade_classifier/positive/1596724932.9526978.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596724981.3780077.jpg b/008_cascade_classifier/positive/1596724981.3780077.jpg deleted file mode 100644 index d7a29b4..0000000 Binary files a/008_cascade_classifier/positive/1596724981.3780077.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725001.129405.jpg b/008_cascade_classifier/positive/1596725001.129405.jpg deleted file mode 100644 index 2990d55..0000000 Binary files a/008_cascade_classifier/positive/1596725001.129405.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725013.2133152.jpg b/008_cascade_classifier/positive/1596725013.2133152.jpg deleted file mode 100644 index b5bba27..0000000 Binary files a/008_cascade_classifier/positive/1596725013.2133152.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725024.112249.jpg b/008_cascade_classifier/positive/1596725024.112249.jpg deleted file mode 100644 index 23a85bd..0000000 Binary files a/008_cascade_classifier/positive/1596725024.112249.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725056.3050916.jpg b/008_cascade_classifier/positive/1596725056.3050916.jpg deleted file mode 100644 index 18ebbf1..0000000 Binary files a/008_cascade_classifier/positive/1596725056.3050916.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725091.287897.jpg b/008_cascade_classifier/positive/1596725091.287897.jpg deleted file mode 100644 index d894a69..0000000 Binary files a/008_cascade_classifier/positive/1596725091.287897.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725112.003226.jpg b/008_cascade_classifier/positive/1596725112.003226.jpg deleted file mode 100644 index ead320b..0000000 Binary files a/008_cascade_classifier/positive/1596725112.003226.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725119.7605214.jpg b/008_cascade_classifier/positive/1596725119.7605214.jpg deleted file mode 100644 index 7bc8f7d..0000000 Binary files a/008_cascade_classifier/positive/1596725119.7605214.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725133.894812.jpg b/008_cascade_classifier/positive/1596725133.894812.jpg deleted file mode 100644 index c3bd58a..0000000 Binary files a/008_cascade_classifier/positive/1596725133.894812.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725145.224427.jpg b/008_cascade_classifier/positive/1596725145.224427.jpg deleted file mode 100644 index b9322b5..0000000 Binary files a/008_cascade_classifier/positive/1596725145.224427.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725160.9995346.jpg b/008_cascade_classifier/positive/1596725160.9995346.jpg deleted file mode 100644 index 087186a..0000000 Binary files a/008_cascade_classifier/positive/1596725160.9995346.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725207.3700407.jpg b/008_cascade_classifier/positive/1596725207.3700407.jpg deleted file mode 100644 index 697c2dc..0000000 Binary files a/008_cascade_classifier/positive/1596725207.3700407.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725236.9265978.jpg b/008_cascade_classifier/positive/1596725236.9265978.jpg deleted file mode 100644 index 9eb8714..0000000 Binary files a/008_cascade_classifier/positive/1596725236.9265978.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725319.6801374.jpg b/008_cascade_classifier/positive/1596725319.6801374.jpg deleted file mode 100644 index 53a95e2..0000000 Binary files a/008_cascade_classifier/positive/1596725319.6801374.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725331.6399002.jpg b/008_cascade_classifier/positive/1596725331.6399002.jpg deleted file mode 100644 index ff932a8..0000000 Binary files a/008_cascade_classifier/positive/1596725331.6399002.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725365.0860987.jpg b/008_cascade_classifier/positive/1596725365.0860987.jpg deleted file mode 100644 index 6c3dca5..0000000 Binary files a/008_cascade_classifier/positive/1596725365.0860987.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725448.8122423.jpg b/008_cascade_classifier/positive/1596725448.8122423.jpg deleted file mode 100644 index 524f585..0000000 Binary files a/008_cascade_classifier/positive/1596725448.8122423.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725482.437604.jpg b/008_cascade_classifier/positive/1596725482.437604.jpg deleted file mode 100644 index 6133e19..0000000 Binary files a/008_cascade_classifier/positive/1596725482.437604.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725504.2595575.jpg b/008_cascade_classifier/positive/1596725504.2595575.jpg deleted file mode 100644 index 15c5bb5..0000000 Binary files a/008_cascade_classifier/positive/1596725504.2595575.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725531.0743005.jpg b/008_cascade_classifier/positive/1596725531.0743005.jpg deleted file mode 100644 index 808a60d..0000000 Binary files a/008_cascade_classifier/positive/1596725531.0743005.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725547.959211.jpg b/008_cascade_classifier/positive/1596725547.959211.jpg deleted file mode 100644 index c6f9dbc..0000000 Binary files a/008_cascade_classifier/positive/1596725547.959211.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725557.5062091.jpg b/008_cascade_classifier/positive/1596725557.5062091.jpg deleted file mode 100644 index 96d19d5..0000000 Binary files a/008_cascade_classifier/positive/1596725557.5062091.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725570.5192885.jpg b/008_cascade_classifier/positive/1596725570.5192885.jpg deleted file mode 100644 index e96049c..0000000 Binary files a/008_cascade_classifier/positive/1596725570.5192885.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725594.7959447.jpg b/008_cascade_classifier/positive/1596725594.7959447.jpg deleted file mode 100644 index cdd99b6..0000000 Binary files a/008_cascade_classifier/positive/1596725594.7959447.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725612.3280354.jpg b/008_cascade_classifier/positive/1596725612.3280354.jpg deleted file mode 100644 index e82a8f2..0000000 Binary files a/008_cascade_classifier/positive/1596725612.3280354.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725623.4045992.jpg b/008_cascade_classifier/positive/1596725623.4045992.jpg deleted file mode 100644 index 34e7b08..0000000 Binary files a/008_cascade_classifier/positive/1596725623.4045992.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725636.845524.jpg b/008_cascade_classifier/positive/1596725636.845524.jpg deleted file mode 100644 index c8e2436..0000000 Binary files a/008_cascade_classifier/positive/1596725636.845524.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725656.2454305.jpg b/008_cascade_classifier/positive/1596725656.2454305.jpg deleted file mode 100644 index 4f59280..0000000 Binary files a/008_cascade_classifier/positive/1596725656.2454305.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725673.0234246.jpg b/008_cascade_classifier/positive/1596725673.0234246.jpg deleted file mode 100644 index 3198e53..0000000 Binary files a/008_cascade_classifier/positive/1596725673.0234246.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725683.22975.jpg b/008_cascade_classifier/positive/1596725683.22975.jpg deleted file mode 100644 index 6e4a4be..0000000 Binary files a/008_cascade_classifier/positive/1596725683.22975.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725691.7093291.jpg b/008_cascade_classifier/positive/1596725691.7093291.jpg deleted file mode 100644 index 3d55aa2..0000000 Binary files a/008_cascade_classifier/positive/1596725691.7093291.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725703.587616.jpg b/008_cascade_classifier/positive/1596725703.587616.jpg deleted file mode 100644 index d5058f4..0000000 Binary files a/008_cascade_classifier/positive/1596725703.587616.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725715.009877.jpg b/008_cascade_classifier/positive/1596725715.009877.jpg deleted file mode 100644 index ac0e9d0..0000000 Binary files a/008_cascade_classifier/positive/1596725715.009877.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725732.5384405.jpg b/008_cascade_classifier/positive/1596725732.5384405.jpg deleted file mode 100644 index e462a49..0000000 Binary files a/008_cascade_classifier/positive/1596725732.5384405.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725787.8061755.jpg b/008_cascade_classifier/positive/1596725787.8061755.jpg deleted file mode 100644 index d63bd40..0000000 Binary files a/008_cascade_classifier/positive/1596725787.8061755.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725809.2862072.jpg b/008_cascade_classifier/positive/1596725809.2862072.jpg deleted file mode 100644 index c127fa1..0000000 Binary files a/008_cascade_classifier/positive/1596725809.2862072.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725850.87147.jpg b/008_cascade_classifier/positive/1596725850.87147.jpg deleted file mode 100644 index 7002398..0000000 Binary files a/008_cascade_classifier/positive/1596725850.87147.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725863.4828084.jpg b/008_cascade_classifier/positive/1596725863.4828084.jpg deleted file mode 100644 index f4343ee..0000000 Binary files a/008_cascade_classifier/positive/1596725863.4828084.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725874.1538086.jpg b/008_cascade_classifier/positive/1596725874.1538086.jpg deleted file mode 100644 index 05ee164..0000000 Binary files a/008_cascade_classifier/positive/1596725874.1538086.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725886.3860302.jpg b/008_cascade_classifier/positive/1596725886.3860302.jpg deleted file mode 100644 index 8e9676c..0000000 Binary files a/008_cascade_classifier/positive/1596725886.3860302.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725967.164775.jpg b/008_cascade_classifier/positive/1596725967.164775.jpg deleted file mode 100644 index 4bb81a1..0000000 Binary files a/008_cascade_classifier/positive/1596725967.164775.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596725983.5232484.jpg b/008_cascade_classifier/positive/1596725983.5232484.jpg deleted file mode 100644 index f2cf5d3..0000000 Binary files a/008_cascade_classifier/positive/1596725983.5232484.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726015.8447444.jpg b/008_cascade_classifier/positive/1596726015.8447444.jpg deleted file mode 100644 index 5885a3c..0000000 Binary files a/008_cascade_classifier/positive/1596726015.8447444.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726023.5497692.jpg b/008_cascade_classifier/positive/1596726023.5497692.jpg deleted file mode 100644 index afae66d..0000000 Binary files a/008_cascade_classifier/positive/1596726023.5497692.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726034.82377.jpg b/008_cascade_classifier/positive/1596726034.82377.jpg deleted file mode 100644 index 773531f..0000000 Binary files a/008_cascade_classifier/positive/1596726034.82377.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726085.3942282.jpg b/008_cascade_classifier/positive/1596726085.3942282.jpg deleted file mode 100644 index 92da347..0000000 Binary files a/008_cascade_classifier/positive/1596726085.3942282.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726109.9420922.jpg b/008_cascade_classifier/positive/1596726109.9420922.jpg deleted file mode 100644 index a0278a5..0000000 Binary files a/008_cascade_classifier/positive/1596726109.9420922.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726116.478095.jpg b/008_cascade_classifier/positive/1596726116.478095.jpg deleted file mode 100644 index 6a27965..0000000 Binary files a/008_cascade_classifier/positive/1596726116.478095.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726192.4596026.jpg b/008_cascade_classifier/positive/1596726192.4596026.jpg deleted file mode 100644 index f8f931a..0000000 Binary files a/008_cascade_classifier/positive/1596726192.4596026.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726416.5257359.jpg b/008_cascade_classifier/positive/1596726416.5257359.jpg deleted file mode 100644 index 97f6188..0000000 Binary files a/008_cascade_classifier/positive/1596726416.5257359.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726430.6089704.jpg b/008_cascade_classifier/positive/1596726430.6089704.jpg deleted file mode 100644 index f343fb5..0000000 Binary files a/008_cascade_classifier/positive/1596726430.6089704.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726442.6422012.jpg b/008_cascade_classifier/positive/1596726442.6422012.jpg deleted file mode 100644 index eee6d32..0000000 Binary files a/008_cascade_classifier/positive/1596726442.6422012.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726453.5657032.jpg b/008_cascade_classifier/positive/1596726453.5657032.jpg deleted file mode 100644 index f0ea829..0000000 Binary files a/008_cascade_classifier/positive/1596726453.5657032.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726474.629971.jpg b/008_cascade_classifier/positive/1596726474.629971.jpg deleted file mode 100644 index 94a5e34..0000000 Binary files a/008_cascade_classifier/positive/1596726474.629971.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726526.9982874.jpg b/008_cascade_classifier/positive/1596726526.9982874.jpg deleted file mode 100644 index ace9ba4..0000000 Binary files a/008_cascade_classifier/positive/1596726526.9982874.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726536.5520952.jpg b/008_cascade_classifier/positive/1596726536.5520952.jpg deleted file mode 100644 index 2051b33..0000000 Binary files a/008_cascade_classifier/positive/1596726536.5520952.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726552.1565392.jpg b/008_cascade_classifier/positive/1596726552.1565392.jpg deleted file mode 100644 index faee3bb..0000000 Binary files a/008_cascade_classifier/positive/1596726552.1565392.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726566.051003.jpg b/008_cascade_classifier/positive/1596726566.051003.jpg deleted file mode 100644 index 70a17f3..0000000 Binary files a/008_cascade_classifier/positive/1596726566.051003.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726579.2008128.jpg b/008_cascade_classifier/positive/1596726579.2008128.jpg deleted file mode 100644 index bc667a8..0000000 Binary files a/008_cascade_classifier/positive/1596726579.2008128.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726626.1602285.jpg b/008_cascade_classifier/positive/1596726626.1602285.jpg deleted file mode 100644 index 7d51127..0000000 Binary files a/008_cascade_classifier/positive/1596726626.1602285.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726654.1444535.jpg b/008_cascade_classifier/positive/1596726654.1444535.jpg deleted file mode 100644 index 914d4b8..0000000 Binary files a/008_cascade_classifier/positive/1596726654.1444535.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726662.3032484.jpg b/008_cascade_classifier/positive/1596726662.3032484.jpg deleted file mode 100644 index ae27de6..0000000 Binary files a/008_cascade_classifier/positive/1596726662.3032484.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726774.8108573.jpg b/008_cascade_classifier/positive/1596726774.8108573.jpg deleted file mode 100644 index 4a23857..0000000 Binary files a/008_cascade_classifier/positive/1596726774.8108573.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726788.6514382.jpg b/008_cascade_classifier/positive/1596726788.6514382.jpg deleted file mode 100644 index 8c09044..0000000 Binary files a/008_cascade_classifier/positive/1596726788.6514382.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726803.447783.jpg b/008_cascade_classifier/positive/1596726803.447783.jpg deleted file mode 100644 index 7316e39..0000000 Binary files a/008_cascade_classifier/positive/1596726803.447783.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726860.5150664.jpg b/008_cascade_classifier/positive/1596726860.5150664.jpg deleted file mode 100644 index b557337..0000000 Binary files a/008_cascade_classifier/positive/1596726860.5150664.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726901.542625.jpg b/008_cascade_classifier/positive/1596726901.542625.jpg deleted file mode 100644 index 82fa861..0000000 Binary files a/008_cascade_classifier/positive/1596726901.542625.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726926.4286637.jpg b/008_cascade_classifier/positive/1596726926.4286637.jpg deleted file mode 100644 index faba3cc..0000000 Binary files a/008_cascade_classifier/positive/1596726926.4286637.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726947.2468882.jpg b/008_cascade_classifier/positive/1596726947.2468882.jpg deleted file mode 100644 index 3bc6876..0000000 Binary files a/008_cascade_classifier/positive/1596726947.2468882.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596726987.308508.jpg b/008_cascade_classifier/positive/1596726987.308508.jpg deleted file mode 100644 index 21e7f85..0000000 Binary files a/008_cascade_classifier/positive/1596726987.308508.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727004.7786236.jpg b/008_cascade_classifier/positive/1596727004.7786236.jpg deleted file mode 100644 index 01fc689..0000000 Binary files a/008_cascade_classifier/positive/1596727004.7786236.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727013.9736161.jpg b/008_cascade_classifier/positive/1596727013.9736161.jpg deleted file mode 100644 index 8120a4c..0000000 Binary files a/008_cascade_classifier/positive/1596727013.9736161.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727037.3830843.jpg b/008_cascade_classifier/positive/1596727037.3830843.jpg deleted file mode 100644 index a903276..0000000 Binary files a/008_cascade_classifier/positive/1596727037.3830843.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727055.9510057.jpg b/008_cascade_classifier/positive/1596727055.9510057.jpg deleted file mode 100644 index 69bb01e..0000000 Binary files a/008_cascade_classifier/positive/1596727055.9510057.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727069.049649.jpg b/008_cascade_classifier/positive/1596727069.049649.jpg deleted file mode 100644 index 5d44e9f..0000000 Binary files a/008_cascade_classifier/positive/1596727069.049649.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727079.762559.jpg b/008_cascade_classifier/positive/1596727079.762559.jpg deleted file mode 100644 index 05993e2..0000000 Binary files a/008_cascade_classifier/positive/1596727079.762559.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727099.340134.jpg b/008_cascade_classifier/positive/1596727099.340134.jpg deleted file mode 100644 index 8df9ea0..0000000 Binary files a/008_cascade_classifier/positive/1596727099.340134.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727137.8729897.jpg b/008_cascade_classifier/positive/1596727137.8729897.jpg deleted file mode 100644 index 8dcbd05..0000000 Binary files a/008_cascade_classifier/positive/1596727137.8729897.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727178.5962224.jpg b/008_cascade_classifier/positive/1596727178.5962224.jpg deleted file mode 100644 index 7cdeb2e..0000000 Binary files a/008_cascade_classifier/positive/1596727178.5962224.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727189.9316707.jpg b/008_cascade_classifier/positive/1596727189.9316707.jpg deleted file mode 100644 index 0ed3978..0000000 Binary files a/008_cascade_classifier/positive/1596727189.9316707.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727203.982804.jpg b/008_cascade_classifier/positive/1596727203.982804.jpg deleted file mode 100644 index 3bc3672..0000000 Binary files a/008_cascade_classifier/positive/1596727203.982804.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727229.6601653.jpg b/008_cascade_classifier/positive/1596727229.6601653.jpg deleted file mode 100644 index 0757b6a..0000000 Binary files a/008_cascade_classifier/positive/1596727229.6601653.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727241.441173.jpg b/008_cascade_classifier/positive/1596727241.441173.jpg deleted file mode 100644 index fe04999..0000000 Binary files a/008_cascade_classifier/positive/1596727241.441173.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727249.654685.jpg b/008_cascade_classifier/positive/1596727249.654685.jpg deleted file mode 100644 index e1d13bb..0000000 Binary files a/008_cascade_classifier/positive/1596727249.654685.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727259.3076847.jpg b/008_cascade_classifier/positive/1596727259.3076847.jpg deleted file mode 100644 index aa223e2..0000000 Binary files a/008_cascade_classifier/positive/1596727259.3076847.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727272.6673214.jpg b/008_cascade_classifier/positive/1596727272.6673214.jpg deleted file mode 100644 index 1cf6ec2..0000000 Binary files a/008_cascade_classifier/positive/1596727272.6673214.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727293.8035266.jpg b/008_cascade_classifier/positive/1596727293.8035266.jpg deleted file mode 100644 index b97b0df..0000000 Binary files a/008_cascade_classifier/positive/1596727293.8035266.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727755.6122937.jpg b/008_cascade_classifier/positive/1596727755.6122937.jpg deleted file mode 100644 index 3ff06c3..0000000 Binary files a/008_cascade_classifier/positive/1596727755.6122937.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727802.904577.jpg b/008_cascade_classifier/positive/1596727802.904577.jpg deleted file mode 100644 index 9d5b787..0000000 Binary files a/008_cascade_classifier/positive/1596727802.904577.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727822.1659546.jpg b/008_cascade_classifier/positive/1596727822.1659546.jpg deleted file mode 100644 index b8bf3a2..0000000 Binary files a/008_cascade_classifier/positive/1596727822.1659546.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727837.7473912.jpg b/008_cascade_classifier/positive/1596727837.7473912.jpg deleted file mode 100644 index 4d82fce..0000000 Binary files a/008_cascade_classifier/positive/1596727837.7473912.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1596727848.3987978.jpg b/008_cascade_classifier/positive/1596727848.3987978.jpg deleted file mode 100644 index 2741c66..0000000 Binary files a/008_cascade_classifier/positive/1596727848.3987978.jpg and /dev/null differ diff --git a/008_cascade_classifier/positive/1597344669.960422.jpg b/008_cascade_classifier/positive/1597344669.960422.jpg deleted file mode 100644 index 7ff17e8..0000000 Binary files a/008_cascade_classifier/positive/1597344669.960422.jpg and /dev/null differ diff --git a/008_cascade_classifier/vision.py b/008_cascade_classifier/vision.py deleted file mode 100644 index b93b030..0000000 --- a/008_cascade_classifier/vision.py +++ /dev/null @@ -1,289 +0,0 @@ -import cv2 as cv -import numpy as np -from hsvfilter import HsvFilter -from edgefilter import EdgeFilter - - -class Vision: - # constants - TRACKBAR_WINDOW = "Trackbars" - - # properties - needle_img = None - needle_w = 0 - needle_h = 0 - method = None - - # constructor - def __init__(self, needle_img_path, method=cv.TM_CCOEFF_NORMED): - if needle_img_path: - # load the image we're trying to match - # https://docs.opencv.org/4.2.0/d4/da8/group__imgcodecs.html - self.needle_img = cv.imread(needle_img_path, cv.IMREAD_UNCHANGED) - - # Save the dimensions of the needle image - self.needle_w = self.needle_img.shape[1] - self.needle_h = self.needle_img.shape[0] - - # There are 6 methods to choose from: - # TM_CCOEFF, TM_CCOEFF_NORMED, TM_CCORR, TM_CCORR_NORMED, TM_SQDIFF, TM_SQDIFF_NORMED - self.method = method - - def find(self, haystack_img, threshold=0.5, max_results=10): - # run the OpenCV algorithm - result = cv.matchTemplate(haystack_img, self.needle_img, self.method) - - # Get the all the positions from the match result that exceed our threshold - locations = np.where(result >= threshold) - locations = list(zip(*locations[::-1])) - #print(locations) - - # if we found no results, return now. this reshape of the empty array allows us to - # concatenate together results without causing an error - if not locations: - return np.array([], dtype=np.int32).reshape(0, 4) - - # You'll notice a lot of overlapping rectangles get drawn. We can eliminate those redundant - # locations by using groupRectangles(). - # First we need to create the list of [x, y, w, h] rectangles - rectangles = [] - for loc in locations: - rect = [int(loc[0]), int(loc[1]), self.needle_w, self.needle_h] - # Add every box to the list twice in order to retain single (non-overlapping) boxes - rectangles.append(rect) - rectangles.append(rect) - # Apply group rectangles. - # The groupThreshold parameter should usually be 1. If you put it at 0 then no grouping is - # done. If you put it at 2 then an object needs at least 3 overlapping rectangles to appear - # in the result. I've set eps to 0.5, which is: - # "Relative difference between sides of the rectangles to merge them into a group." - rectangles, weights = cv.groupRectangles(rectangles, groupThreshold=1, eps=0.5) - #print(rectangles) - - # for performance reasons, return a limited number of results. - # these aren't necessarily the best results. - if len(rectangles) > max_results: - print('Warning: too many results, raise the threshold.') - rectangles = rectangles[:max_results] - - return rectangles - - # given a list of [x, y, w, h] rectangles returned by find(), convert those into a list of - # [x, y] positions in the center of those rectangles where we can click on those found items - def get_click_points(self, rectangles): - points = [] - - # Loop over all the rectangles - for (x, y, w, h) in rectangles: - # Determine the center position - center_x = x + int(w/2) - center_y = y + int(h/2) - # Save the points - points.append((center_x, center_y)) - - return points - - # given a list of [x, y, w, h] rectangles and a canvas image to draw on, return an image with - # all of those rectangles drawn - def draw_rectangles(self, haystack_img, rectangles): - # these colors are actually BGR - line_color = (0, 255, 0) - line_type = cv.LINE_4 - - for (x, y, w, h) in rectangles: - # determine the box positions - top_left = (x, y) - bottom_right = (x + w, y + h) - # draw the box - cv.rectangle(haystack_img, top_left, bottom_right, line_color, lineType=line_type) - - return haystack_img - - # given a list of [x, y] positions and a canvas image to draw on, return an image with all - # of those click points drawn on as crosshairs - def draw_crosshairs(self, haystack_img, points): - # these colors are actually BGR - marker_color = (255, 0, 255) - marker_type = cv.MARKER_CROSS - - for (center_x, center_y) in points: - # draw the center point - cv.drawMarker(haystack_img, (center_x, center_y), marker_color, marker_type) - - return haystack_img - - # create gui window with controls for adjusting arguments in real-time - def init_control_gui(self): - cv.namedWindow(self.TRACKBAR_WINDOW, cv.WINDOW_NORMAL) - cv.resizeWindow(self.TRACKBAR_WINDOW, 350, 700) - - # required callback. we'll be using getTrackbarPos() to do lookups - # instead of using the callback. - def nothing(position): - pass - - # create trackbars for bracketing. - # OpenCV scale for HSV is H: 0-179, S: 0-255, V: 0-255 - cv.createTrackbar('HMin', self.TRACKBAR_WINDOW, 0, 179, nothing) - cv.createTrackbar('SMin', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('VMin', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('HMax', self.TRACKBAR_WINDOW, 0, 179, nothing) - cv.createTrackbar('SMax', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('VMax', self.TRACKBAR_WINDOW, 0, 255, nothing) - # Set default value for Max HSV trackbars - cv.setTrackbarPos('HMax', self.TRACKBAR_WINDOW, 179) - cv.setTrackbarPos('SMax', self.TRACKBAR_WINDOW, 255) - cv.setTrackbarPos('VMax', self.TRACKBAR_WINDOW, 255) - - # trackbars for increasing/decreasing saturation and value - cv.createTrackbar('SAdd', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('SSub', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('VAdd', self.TRACKBAR_WINDOW, 0, 255, nothing) - cv.createTrackbar('VSub', self.TRACKBAR_WINDOW, 0, 255, nothing) - - # trackbars for edge creation - cv.createTrackbar('KernelSize', self.TRACKBAR_WINDOW, 1, 30, nothing) - cv.createTrackbar('ErodeIter', self.TRACKBAR_WINDOW, 1, 5, nothing) - cv.createTrackbar('DilateIter', self.TRACKBAR_WINDOW, 1, 5, nothing) - cv.createTrackbar('Canny1', self.TRACKBAR_WINDOW, 0, 200, nothing) - cv.createTrackbar('Canny2', self.TRACKBAR_WINDOW, 0, 500, nothing) - # Set default value for Canny trackbars - cv.setTrackbarPos('KernelSize', self.TRACKBAR_WINDOW, 5) - cv.setTrackbarPos('Canny1', self.TRACKBAR_WINDOW, 100) - cv.setTrackbarPos('Canny2', self.TRACKBAR_WINDOW, 200) - - # returns an HSV filter object based on the control GUI values - def get_hsv_filter_from_controls(self): - # Get current positions of all trackbars - hsv_filter = HsvFilter() - hsv_filter.hMin = cv.getTrackbarPos('HMin', self.TRACKBAR_WINDOW) - hsv_filter.sMin = cv.getTrackbarPos('SMin', self.TRACKBAR_WINDOW) - hsv_filter.vMin = cv.getTrackbarPos('VMin', self.TRACKBAR_WINDOW) - hsv_filter.hMax = cv.getTrackbarPos('HMax', self.TRACKBAR_WINDOW) - hsv_filter.sMax = cv.getTrackbarPos('SMax', self.TRACKBAR_WINDOW) - hsv_filter.vMax = cv.getTrackbarPos('VMax', self.TRACKBAR_WINDOW) - hsv_filter.sAdd = cv.getTrackbarPos('SAdd', self.TRACKBAR_WINDOW) - hsv_filter.sSub = cv.getTrackbarPos('SSub', self.TRACKBAR_WINDOW) - hsv_filter.vAdd = cv.getTrackbarPos('VAdd', self.TRACKBAR_WINDOW) - hsv_filter.vSub = cv.getTrackbarPos('VSub', self.TRACKBAR_WINDOW) - return hsv_filter - - # returns a Canny edge filter object based on the control GUI values - def get_edge_filter_from_controls(self): - # Get current positions of all trackbars - edge_filter = EdgeFilter() - edge_filter.kernelSize = cv.getTrackbarPos('KernelSize', self.TRACKBAR_WINDOW) - edge_filter.erodeIter = cv.getTrackbarPos('ErodeIter', self.TRACKBAR_WINDOW) - edge_filter.dilateIter = cv.getTrackbarPos('DilateIter', self.TRACKBAR_WINDOW) - edge_filter.canny1 = cv.getTrackbarPos('Canny1', self.TRACKBAR_WINDOW) - edge_filter.canny2 = cv.getTrackbarPos('Canny2', self.TRACKBAR_WINDOW) - return edge_filter - - # given an image and an HSV filter, apply the filter and return the resulting image. - # if a filter is not supplied, the control GUI trackbars will be used - def apply_hsv_filter(self, original_image, hsv_filter=None): - # convert image to HSV - hsv = cv.cvtColor(original_image, cv.COLOR_BGR2HSV) - - # if we haven't been given a defined filter, use the filter values from the GUI - if not hsv_filter: - hsv_filter = self.get_hsv_filter_from_controls() - - # add/subtract saturation and value - h, s, v = cv.split(hsv) - s = self.shift_channel(s, hsv_filter.sAdd) - s = self.shift_channel(s, -hsv_filter.sSub) - v = self.shift_channel(v, hsv_filter.vAdd) - v = self.shift_channel(v, -hsv_filter.vSub) - hsv = cv.merge([h, s, v]) - - # Set minimum and maximum HSV values to display - lower = np.array([hsv_filter.hMin, hsv_filter.sMin, hsv_filter.vMin]) - upper = np.array([hsv_filter.hMax, hsv_filter.sMax, hsv_filter.vMax]) - # Apply the thresholds - mask = cv.inRange(hsv, lower, upper) - result = cv.bitwise_and(hsv, hsv, mask=mask) - - # convert back to BGR for imshow() to display it properly - img = cv.cvtColor(result, cv.COLOR_HSV2BGR) - - return img - - # given an image and a Canny edge filter, apply the filter and return the resulting image. - # if a filter is not supplied, the control GUI trackbars will be used - def apply_edge_filter(self, original_image, edge_filter=None): - # if we haven't been given a defined filter, use the filter values from the GUI - if not edge_filter: - edge_filter = self.get_edge_filter_from_controls() - - kernel = np.ones((edge_filter.kernelSize, edge_filter.kernelSize), np.uint8) - eroded_image = cv.erode(original_image, kernel, iterations=edge_filter.erodeIter) - dilated_image = cv.dilate(eroded_image, kernel, iterations=edge_filter.dilateIter) - - # canny edge detection - result = cv.Canny(dilated_image, edge_filter.canny1, edge_filter.canny2) - - # convert single channel image back to BGR - img = cv.cvtColor(result, cv.COLOR_GRAY2BGR) - - return img - - # apply adjustments to an HSV channel - # https://stackoverflow.com/questions/49697363/shifting-hsv-pixel-values-in-python-using-numpy - def shift_channel(self, c, amount): - if amount > 0: - lim = 255 - amount - c[c >= lim] = 255 - c[c < lim] += amount - elif amount < 0: - amount = -amount - lim = amount - c[c <= lim] = 0 - c[c > lim] -= amount - return c - - def match_keypoints(self, original_image, patch_size=32): - min_match_count = 5 - - orb = cv.ORB_create(edgeThreshold=0, patchSize=patch_size) - keypoints_needle, descriptors_needle = orb.detectAndCompute(self.needle_img, None) - orb2 = cv.ORB_create(edgeThreshold=0, patchSize=patch_size, nfeatures=2000) - keypoints_haystack, descriptors_haystack = orb2.detectAndCompute(original_image, None) - - FLANN_INDEX_LSH = 6 - index_params = dict(algorithm=FLANN_INDEX_LSH, - table_number=6, - key_size=12, - multi_probe_level=1) - - search_params = dict(checks=50) - - try: - flann = cv.FlannBasedMatcher(index_params, search_params) - matches = flann.knnMatch(descriptors_needle, descriptors_haystack, k=2) - except cv.error: - return None, None, [], [], None - - # store all the good matches as per Lowe's ratio test. - good = [] - points = [] - - for pair in matches: - if len(pair) == 2: - if pair[0].distance < 0.7*pair[1].distance: - good.append(pair[0]) - - if len(good) > min_match_count: - print('match %03d, kp %03d' % (len(good), len(keypoints_needle))) - for match in good: - points.append(keypoints_haystack[match.trainIdx].pt) - #print(points) - - return keypoints_needle, keypoints_haystack, good, points - - def centeroid(self, point_list): - point_list = np.asarray(point_list, dtype=np.int32) - length = point_list.shape[0] - sum_x = np.sum(point_list[:, 0]) - sum_y = np.sum(point_list[:, 1]) - return [np.floor_divide(sum_x, length), np.floor_divide(sum_y, length)] diff --git a/008_cascade_classifier/windowcapture.py b/008_cascade_classifier/windowcapture.py deleted file mode 100644 index 0919856..0000000 --- a/008_cascade_classifier/windowcapture.py +++ /dev/null @@ -1,98 +0,0 @@ -import numpy as np -import win32gui, win32ui, win32con - - -class WindowCapture: - - # properties - w = 0 - h = 0 - hwnd = None - cropped_x = 0 - cropped_y = 0 - offset_x = 0 - offset_y = 0 - - # constructor - def __init__(self, window_name=None): - # find the handle for the window we want to capture. - # if no window name is given, capture the entire screen - if window_name is None: - self.hwnd = win32gui.GetDesktopWindow() - else: - self.hwnd = win32gui.FindWindow(None, window_name) - if not self.hwnd: - raise Exception('Window not found: {}'.format(window_name)) - - # get the window size - window_rect = win32gui.GetWindowRect(self.hwnd) - self.w = window_rect[2] - window_rect[0] - self.h = window_rect[3] - window_rect[1] - - # account for the window border and titlebar and cut them off - border_pixels = 8 - titlebar_pixels = 30 - self.w = self.w - (border_pixels * 2) - self.h = self.h - titlebar_pixels - border_pixels - self.cropped_x = border_pixels - self.cropped_y = titlebar_pixels - - # set the cropped coordinates offset so we can translate screenshot - # images into actual screen positions - self.offset_x = window_rect[0] + self.cropped_x - self.offset_y = window_rect[1] + self.cropped_y - - def get_screenshot(self): - - # get the window image data - wDC = win32gui.GetWindowDC(self.hwnd) - dcObj = win32ui.CreateDCFromHandle(wDC) - cDC = dcObj.CreateCompatibleDC() - dataBitMap = win32ui.CreateBitmap() - dataBitMap.CreateCompatibleBitmap(dcObj, self.w, self.h) - cDC.SelectObject(dataBitMap) - cDC.BitBlt((0, 0), (self.w, self.h), dcObj, (self.cropped_x, self.cropped_y), win32con.SRCCOPY) - - # convert the raw data into a format opencv can read - #dataBitMap.SaveBitmapFile(cDC, 'debug.bmp') - signedIntsArray = dataBitMap.GetBitmapBits(True) - img = np.fromstring(signedIntsArray, dtype='uint8') - img.shape = (self.h, self.w, 4) - - # free resources - dcObj.DeleteDC() - cDC.DeleteDC() - win32gui.ReleaseDC(self.hwnd, wDC) - win32gui.DeleteObject(dataBitMap.GetHandle()) - - # drop the alpha channel, or cv.matchTemplate() will throw an error like: - # error: (-215:Assertion failed) (depth == CV_8U || depth == CV_32F) && type == _templ.type() - # && _img.dims() <= 2 in function 'cv::matchTemplate' - img = img[...,:3] - - # make image C_CONTIGUOUS to avoid errors that look like: - # File ... in draw_rectangles - # TypeError: an integer is required (got type tuple) - # see the discussion here: - # https://github.com/opencv/opencv/issues/14866#issuecomment-580207109 - img = np.ascontiguousarray(img) - - return img - - # find the name of the window you're interested in. - # once you have it, update window_capture() - # https://stackoverflow.com/questions/55547940/how-to-get-a-list-of-the-name-of-every-open-window - @staticmethod - def list_window_names(): - def winEnumHandler(hwnd, ctx): - if win32gui.IsWindowVisible(hwnd): - print(hex(hwnd), win32gui.GetWindowText(hwnd)) - win32gui.EnumWindows(winEnumHandler, None) - - # translate a pixel position on a screenshot image to a pixel position on the screen. - # pos = (x, y) - # WARNING: if you move the window being captured after execution is started, this will - # return incorrect coordinates, because the window position is only calculated in - # the __init__ constructor. - def get_screen_position(self, pos): - return (pos[0] + self.offset_x, pos[1] + self.offset_y) diff --git a/009_bot/bot.py b/009_bot/bot.py deleted file mode 100644 index 617b019..0000000 --- a/009_bot/bot.py +++ /dev/null @@ -1,270 +0,0 @@ -import cv2 as cv -import pyautogui -from time import sleep, time -from threading import Thread, Lock -from math import sqrt - - -class BotState: - INITIALIZING = 0 - SEARCHING = 1 - MOVING = 2 - MINING = 3 - BACKTRACKING = 4 - - -class AlbionBot: - - # constants - INITIALIZING_SECONDS = 6 - MINING_SECONDS = 14 - MOVEMENT_STOPPED_THRESHOLD = 0.975 - IGNORE_RADIUS = 130 - TOOLTIP_MATCH_THRESHOLD = 0.72 - - # threading properties - stopped = True - lock = None - - # properties - state = None - targets = [] - screenshot = None - timestamp = None - movement_screenshot = None - window_offset = (0,0) - window_w = 0 - window_h = 0 - limestone_tooltip = None - click_history = [] - - def __init__(self, window_offset, window_size): - # create a thread lock object - self.lock = Lock() - - # for translating window positions into screen positions, it's easier to just - # get the offsets and window size from WindowCapture rather than passing in - # the whole object - self.window_offset = window_offset - self.window_w = window_size[0] - self.window_h = window_size[1] - - # pre-load the needle image used to confirm our object detection - self.limestone_tooltip = cv.imread('limestone_tooltip.jpg', cv.IMREAD_UNCHANGED) - - # start bot in the initializing mode to allow us time to get setup. - # mark the time at which this started so we know when to complete it - self.state = BotState.INITIALIZING - self.timestamp = time() - - def click_next_target(self): - # 1. order targets by distance from center - # loop: - # 2. hover over the nearest target - # 3. confirm that it's limestone via the tooltip - # 4. if it's not, check the next target - # endloop - # 5. if no target was found return false - # 6. click on the found target and return true - targets = self.targets_ordered_by_distance(self.targets) - - target_i = 0 - found_limestone = False - while not found_limestone and target_i < len(targets): - # if we stopped our script, exit this loop - if self.stopped: - break - - # load up the next target in the list and convert those coordinates - # that are relative to the game screenshot to a position on our - # screen - target_pos = targets[target_i] - screen_x, screen_y = self.get_screen_position(target_pos) - print('Moving mouse to x:{} y:{}'.format(screen_x, screen_y)) - - # move the mouse - pyautogui.moveTo(x=screen_x, y=screen_y) - # short pause to let the mouse movement complete and allow - # time for the tooltip to appear - sleep(1.250) - # confirm limestone tooltip - if self.confirm_tooltip(target_pos): - print('Click on confirmed target at x:{} y:{}'.format(screen_x, screen_y)) - found_limestone = True - pyautogui.click() - # save this position to the click history - self.click_history.append(target_pos) - target_i += 1 - - return found_limestone - - def have_stopped_moving(self): - # if we haven't stored a screenshot to compare to, do that first - if self.movement_screenshot is None: - self.movement_screenshot = self.screenshot.copy() - return False - - # compare the old screenshot to the new screenshot - result = cv.matchTemplate(self.screenshot, self.movement_screenshot, cv.TM_CCOEFF_NORMED) - # we only care about the value when the two screenshots are laid perfectly over one - # another, so the needle position is (0, 0). since both images are the same size, this - # should be the only result that exists anyway - similarity = result[0][0] - print('Movement detection similarity: {}'.format(similarity)) - - if similarity >= self.MOVEMENT_STOPPED_THRESHOLD: - # pictures look similar, so we've probably stopped moving - print('Movement detected stop') - return True - - # looks like we're still moving. - # use this new screenshot to compare to the next one - self.movement_screenshot = self.screenshot.copy() - return False - - def targets_ordered_by_distance(self, targets): - # our character is always in the center of the screen - my_pos = (self.window_w / 2, self.window_h / 2) - # searched "python order points by distance from point" - # simply uses the pythagorean theorem - # https://stackoverflow.com/a/30636138/4655368 - def pythagorean_distance(pos): - return sqrt((pos[0] - my_pos[0])**2 + (pos[1] - my_pos[1])**2) - targets.sort(key=pythagorean_distance) - - # print(my_pos) - # print(targets) - # for t in targets: - # print(pythagorean_distance(t)) - - # ignore targets at are too close to our character (within 130 pixels) to avoid - # re-clicking a deposit we just mined - targets = [t for t in targets if pythagorean_distance(t) > self.IGNORE_RADIUS] - - return targets - - def confirm_tooltip(self, target_position): - # check the current screenshot for the limestone tooltip using match template - result = cv.matchTemplate(self.screenshot, self.limestone_tooltip, cv.TM_CCOEFF_NORMED) - # get the best match postition - min_val, max_val, min_loc, max_loc = cv.minMaxLoc(result) - # if we can closely match the tooltip image, consider the object found - if max_val >= self.TOOLTIP_MATCH_THRESHOLD: - # print('Tooltip found in image at {}'.format(max_loc)) - # screen_loc = self.get_screen_position(max_loc) - # print('Found on screen at {}'.format(screen_loc)) - # mouse_position = pyautogui.position() - # print('Mouse on screen at {}'.format(mouse_position)) - # offset = (mouse_position[0] - screen_loc[0], mouse_position[1] - screen_loc[1]) - # print('Offset calculated as x: {} y: {}'.format(offset[0], offset[1])) - # the offset I always got was Offset calculated as x: -22 y: -29 - return True - #print('Tooltip not found.') - return False - - def click_backtrack(self): - # pop the top item off the clicked points stack. this will be the click that - # brought us to our current location. - last_click = self.click_history.pop() - # to undo this click, we must mirror it across the center point. so if our - # character is at the middle of the screen at ex. (100, 100), and our last - # click was at (120, 120), then to undo this we must now click at (80, 80). - # our character is always in the center of the screen - my_pos = (self.window_w / 2, self.window_h / 2) - mirrored_click_x = my_pos[0] - (last_click[0] - my_pos[0]) - mirrored_click_y = my_pos[1] - (last_click[1] - my_pos[1]) - # convert this screenshot position to a screen position - screen_x, screen_y = self.get_screen_position((mirrored_click_x, mirrored_click_y)) - print('Backtracking to x:{} y:{}'.format(screen_x, screen_y)) - pyautogui.moveTo(x=screen_x, y=screen_y) - # short pause to let the mouse movement complete - sleep(0.500) - pyautogui.click() - - # translate a pixel position on a screenshot image to a pixel position on the screen. - # pos = (x, y) - # WARNING: if you move the window being captured after execution is started, this will - # return incorrect coordinates, because the window position is only calculated in - # the WindowCapture __init__ constructor. - def get_screen_position(self, pos): - return (pos[0] + self.window_offset[0], pos[1] + self.window_offset[1]) - - # threading methods - - def update_targets(self, targets): - self.lock.acquire() - self.targets = targets - self.lock.release() - - def update_screenshot(self, screenshot): - self.lock.acquire() - self.screenshot = screenshot - self.lock.release() - - def start(self): - self.stopped = False - t = Thread(target=self.run) - t.start() - - def stop(self): - self.stopped = True - - # main logic controller - def run(self): - while not self.stopped: - if self.state == BotState.INITIALIZING: - # do no bot actions until the startup waiting period is complete - if time() > self.timestamp + self.INITIALIZING_SECONDS: - # start searching when the waiting period is over - self.lock.acquire() - self.state = BotState.SEARCHING - self.lock.release() - - elif self.state == BotState.SEARCHING: - # check the given click point targets, confirm a limestone deposit, - # then click it. - success = self.click_next_target() - # if not successful, try one more time - if not success: - success = self.click_next_target() - - # if successful, switch state to moving - # if not, backtrack or hold the current position - if success: - self.lock.acquire() - self.state = BotState.MOVING - self.lock.release() - elif len(self.click_history) > 0: - self.click_backtrack() - self.lock.acquire() - self.state = BotState.BACKTRACKING - self.lock.release() - else: - # stay in place and keep searching - pass - - elif self.state == BotState.MOVING or self.state == BotState.BACKTRACKING: - # see if we've stopped moving yet by comparing the current pixel mesh - # to the previously observed mesh - if not self.have_stopped_moving(): - # wait a short time to allow for the character position to change - sleep(0.500) - else: - # reset the timestamp marker to the current time. switch state - # to mining if we clicked on a deposit, or search again if we - # backtracked - self.lock.acquire() - if self.state == BotState.MOVING: - self.timestamp = time() - self.state = BotState.MINING - elif self.state == BotState.BACKTRACKING: - self.state = BotState.SEARCHING - self.lock.release() - - elif self.state == BotState.MINING: - # see if we're done mining. just wait some amount of time - if time() > self.timestamp + self.MINING_SECONDS: - # return to the searching state - self.lock.acquire() - self.state = BotState.SEARCHING - self.lock.release() diff --git a/009_bot/detection.py b/009_bot/detection.py deleted file mode 100644 index 2dda9cf..0000000 --- a/009_bot/detection.py +++ /dev/null @@ -1,43 +0,0 @@ -import cv2 as cv -from threading import Thread, Lock - - -class Detection: - - # threading properties - stopped = True - lock = None - rectangles = [] - # properties - cascade = None - screenshot = None - - def __init__(self, model_file_path): - # create a thread lock object - self.lock = Lock() - # load the trained model - self.cascade = cv.CascadeClassifier(model_file_path) - - def update(self, screenshot): - self.lock.acquire() - self.screenshot = screenshot - self.lock.release() - - def start(self): - self.stopped = False - t = Thread(target=self.run) - t.start() - - def stop(self): - self.stopped = True - - def run(self): - # TODO: you can write your own time/iterations calculation to determine how fast this is - while not self.stopped: - if not self.screenshot is None: - # do object detection - rectangles = self.cascade.detectMultiScale(self.screenshot) - # lock the thread while updating the results - self.lock.acquire() - self.rectangles = rectangles - self.lock.release() diff --git a/009_bot/limestone_model_final.xml b/009_bot/limestone_model_final.xml deleted file mode 100644 index 99a6398..0000000 --- a/009_bot/limestone_model_final.xml +++ /dev/null @@ -1,1737 +0,0 @@ - - - - BOOST - HAAR - 24 - 24 - - GAB - 9.9900001287460327e-01 - 4.0000000596046448e-01 - 9.4999999999999996e-01 - 1 - 100 - - 0 - 1 - BASIC - 12 - - - <_> - 3 - -1.2815593481063843e+00 - - <_> - - 0 -1 29 -2.6531535387039185e-01 - - -9.7701150178909302e-01 1.5151515603065491e-01 - <_> - - 0 -1 73 3.3036179840564728e-02 - - -7.2519451379776001e-01 8.4022516012191772e-01 - <_> - - 0 -1 113 -4.1663810610771179e-02 - - -9.3661451339721680e-01 4.2064669728279114e-01 - - <_> - 7 - -1.5778355598449707e+00 - - <_> - - 0 -1 49 1.4340322464704514e-02 - - -8.1261950731277466e-01 3.2467532157897949e-01 - <_> - - 0 -1 6 -7.0590246468782425e-03 - - 6.8404257297515869e-01 -4.9814325571060181e-01 - <_> - - 0 -1 129 1.0002681985497475e-02 - - -3.5684755444526672e-01 7.5740498304367065e-01 - <_> - - 0 -1 14 -2.1922769024968147e-02 - - 7.6397258043289185e-01 -3.1399649381637573e-01 - <_> - - 0 -1 78 -2.2847860236652195e-04 - - 4.7088450193405151e-01 -5.7153099775314331e-01 - <_> - - 0 -1 51 1.2028008699417114e-02 - - -3.4323534369468689e-01 6.4455568790435791e-01 - <_> - - 0 -1 71 -4.8528384417295456e-02 - - -9.2709094285964966e-01 2.7612203359603882e-01 - - <_> - 8 - -1.6270295381546021e+00 - - <_> - - 0 -1 60 5.1783770322799683e-02 - - -8.1818181276321411e-01 4.4444444775581360e-01 - <_> - - 0 -1 126 3.4453733824193478e-03 - - -5.2110916376113892e-01 3.8695532083511353e-01 - <_> - - 0 -1 86 -2.3107819259166718e-02 - - 1.8288460373878479e-01 -9.5233714580535889e-01 - <_> - - 0 -1 9 -8.4966458380222321e-03 - - 6.5764582157135010e-01 -2.9777401685714722e-01 - <_> - - 0 -1 52 5.5540574248880148e-04 - - -2.5696116685867310e-01 5.2375447750091553e-01 - <_> - - 0 -1 110 3.7999920547008514e-02 - - -2.2597408294677734e-01 6.5327495336532593e-01 - <_> - - 0 -1 38 -4.8657096922397614e-02 - - 6.8704473972320557e-01 -2.1762873232364655e-01 - <_> - - 0 -1 34 3.7764841690659523e-03 - - -3.5153421759605408e-01 5.6150972843170166e-01 - - <_> - 13 - -1.5087051391601563e+00 - - <_> - - 0 -1 89 3.6675274372100830e-02 - - -7.9358714818954468e-01 -3.9603959769010544e-02 - <_> - - 0 -1 7 -2.5295931845903397e-02 - - 2.9524472355842590e-01 -5.1345789432525635e-01 - <_> - - 0 -1 30 -2.0656153559684753e-02 - - 4.0355071425437927e-01 -3.2067385315895081e-01 - <_> - - 0 -1 47 1.3974596746265888e-02 - - -2.6310223340988159e-01 5.7496279478073120e-01 - <_> - - 0 -1 128 1.4068853110074997e-02 - - -1.8699720501899719e-01 7.7813905477523804e-01 - <_> - - 0 -1 55 -1.1953005567193031e-02 - - 4.4992053508758545e-01 -2.6490813493728638e-01 - <_> - - 0 -1 119 5.5090682581067085e-03 - - -2.2897675633430481e-01 5.0835984945297241e-01 - <_> - - 0 -1 81 -3.7762962281703949e-02 - - -9.6912604570388794e-01 1.6655203700065613e-01 - <_> - - 0 -1 45 1.9030734896659851e-02 - - -2.2931246459484100e-01 5.8443081378936768e-01 - <_> - - 0 -1 64 2.1625359659083188e-04 - - -3.2120153307914734e-01 3.8072818517684937e-01 - <_> - - 0 -1 20 -1.2257000431418419e-02 - - 4.5801120996475220e-01 -2.8640499711036682e-01 - <_> - - 0 -1 120 -9.1817528009414673e-03 - - 5.6792247295379639e-01 -1.7843975126743317e-01 - <_> - - 0 -1 103 -1.8793119117617607e-02 - - 4.9504637718200684e-01 -2.5712665915489197e-01 - - <_> - 11 - -1.4701702594757080e+00 - - <_> - - 0 -1 3 5.8097988367080688e-02 - - -7.2848272323608398e-01 2.8767123818397522e-01 - <_> - - 0 -1 105 1.6994535923004150e-02 - - -4.4815555214881897e-01 3.1635886430740356e-01 - <_> - - 0 -1 25 -3.2376497983932495e-02 - - 4.4355374574661255e-01 -3.0353689193725586e-01 - <_> - - 0 -1 107 4.4868811964988708e-03 - - -2.5752559304237366e-01 5.3392666578292847e-01 - <_> - - 0 -1 116 1.3828460127115250e-02 - - -2.3867878317832947e-01 5.2954345941543579e-01 - <_> - - 0 -1 83 -1.0387969203293324e-02 - - 3.0829572677612305e-01 -4.8065263032913208e-01 - <_> - - 0 -1 8 -4.0221336483955383e-01 - - 1.7585287988185883e-01 -9.7357034683227539e-01 - <_> - - 0 -1 54 -3.6796718835830688e-02 - - -6.4116781949996948e-01 1.9513781368732452e-01 - <_> - - 0 -1 11 -3.4659340977668762e-02 - - 7.2359263896942139e-01 -1.9654741883277893e-01 - <_> - - 0 -1 96 -5.2678319625556469e-03 - - 3.6885800957679749e-01 -3.3221104741096497e-01 - <_> - - 0 -1 44 -2.5319552514702082e-03 - - 4.7757115960121155e-01 -2.5287047028541565e-01 - - <_> - 10 - -1.3089859485626221e+00 - - <_> - - 0 -1 18 5.2907690405845642e-03 - - -7.6158940792083740e-01 3.4965034574270248e-02 - <_> - - 0 -1 2 -8.7920054793357849e-03 - - 3.7156713008880615e-01 -3.6699345707893372e-01 - <_> - - 0 -1 114 2.7730345726013184e-02 - - -2.7396458387374878e-01 4.7486495971679688e-01 - <_> - - 0 -1 59 -5.0081420689821243e-02 - - 1.1815773695707321e-01 -9.2557746171951294e-01 - <_> - - 0 -1 50 -5.2779637277126312e-02 - - -1.8923561275005341e-01 6.2627190351486206e-01 - <_> - - 0 -1 37 -1.1504447087645531e-02 - - 6.8990832567214966e-01 -1.6575154662132263e-01 - <_> - - 0 -1 43 -4.4810341205447912e-04 - - 3.2456266880035400e-01 -2.9157385230064392e-01 - <_> - - 0 -1 57 2.3839831352233887e-02 - - 9.5953084528446198e-02 -9.4295924901962280e-01 - <_> - - 0 -1 33 -1.4092427445575595e-03 - - 5.5520117282867432e-01 -2.0291884243488312e-01 - <_> - - 0 -1 36 -1.0970374569296837e-02 - - -8.3941036462783813e-01 1.1279396712779999e-01 - - <_> - 11 - -1.1969076395034790e+00 - - <_> - - 0 -1 112 -1.7543733119964600e-02 - - -2.1052631735801697e-01 -8.2142859697341919e-01 - <_> - - 0 -1 23 -8.3853630349040031e-03 - - 2.1931529045104980e-01 -4.6866944432258606e-01 - <_> - - 0 -1 70 -1.5655077993869781e-02 - - 2.0215572416782379e-01 -5.2129101753234863e-01 - <_> - - 0 -1 56 5.7208468206226826e-04 - - -2.8226605057716370e-01 3.9611354470252991e-01 - <_> - - 0 -1 26 -4.0774211287498474e-02 - - 4.7153508663177490e-01 -1.8893110752105713e-01 - <_> - - 0 -1 106 -1.6591936349868774e-02 - - 4.1324305534362793e-01 -3.3773151040077209e-01 - <_> - - 0 -1 21 1.5747562050819397e-02 - - -2.0940901339054108e-01 5.3346717357635498e-01 - <_> - - 0 -1 80 3.4584333188831806e-03 - - -2.1836459636688232e-01 4.8452955484390259e-01 - <_> - - 0 -1 127 1.2953504920005798e-02 - - -1.8292598426342010e-01 5.0049030780792236e-01 - <_> - - 0 -1 117 -7.9962313175201416e-03 - - 5.6026089191436768e-01 -1.9084297120571136e-01 - <_> - - 0 -1 131 -9.1765663819387555e-04 - - -5.3433579206466675e-01 2.0260965824127197e-01 - - <_> - 14 - -1.5340468883514404e+00 - - <_> - - 0 -1 35 1.9439160823822021e-02 - - -7.8031086921691895e-01 -2.0000000298023224e-01 - <_> - - 0 -1 17 1.4534162357449532e-02 - - -3.5326647758483887e-01 3.7831932306289673e-01 - <_> - - 0 -1 99 -4.9835231155157089e-02 - - -8.1070756912231445e-01 1.1001287400722504e-01 - <_> - - 0 -1 109 -1.0803632438182831e-02 - - 1.5099881589412689e-01 -7.8630656003952026e-01 - <_> - - 0 -1 85 -1.1284489184617996e-02 - - 2.5855007767677307e-01 -3.5162982344627380e-01 - <_> - - 0 -1 94 -2.1756377071142197e-02 - - -7.6442658901214600e-01 1.1108461767435074e-01 - <_> - - 0 -1 39 -2.0368300378322601e-02 - - 4.4156181812286377e-01 -2.2962969541549683e-01 - <_> - - 0 -1 65 5.5380766279995441e-03 - - -2.2159671783447266e-01 4.0964427590370178e-01 - <_> - - 0 -1 121 -1.6451325267553329e-02 - - -1.5554983913898468e-01 5.4646855592727661e-01 - <_> - - 0 -1 91 -8.8985881302505732e-04 - - 3.5926350951194763e-01 -2.7782326936721802e-01 - <_> - - 0 -1 41 -3.5103228874504566e-03 - - 3.9585193991661072e-01 -2.3765103518962860e-01 - <_> - - 0 -1 133 -2.4599839001893997e-02 - - 3.5694575309753418e-01 -2.4165844917297363e-01 - <_> - - 0 -1 98 6.3818749040365219e-03 - - -1.8454258143901825e-01 4.1676977276802063e-01 - <_> - - 0 -1 115 -2.4639917537570000e-02 - - 6.5221750736236572e-01 -1.5385587513446808e-01 - - <_> - 14 - -1.4048395156860352e+00 - - <_> - - 0 -1 79 -1.1194668710231781e-02 - - -2.5688073039054871e-01 -7.5763750076293945e-01 - <_> - - 0 -1 10 -1.2587085366249084e-02 - - 4.0870183706283569e-01 -3.1488448381423950e-01 - <_> - - 0 -1 53 -3.5880759358406067e-01 - - -6.1195844411849976e-01 1.7459505796432495e-01 - <_> - - 0 -1 134 -1.6222944483160973e-02 - - 3.5100319981575012e-01 -3.0121064186096191e-01 - <_> - - 0 -1 77 -7.6989643275737762e-05 - - 2.3703606426715851e-01 -4.2212566733360291e-01 - <_> - - 0 -1 40 -9.4061775598675013e-04 - - 4.1122129559516907e-01 -1.8063741922378540e-01 - <_> - - 0 -1 27 5.8695450425148010e-03 - - -1.9567514955997467e-01 4.6317130327224731e-01 - <_> - - 0 -1 61 -5.6588794104754925e-03 - - 4.4456613063812256e-01 -1.9832246005535126e-01 - <_> - - 0 -1 82 -1.0015970095992088e-02 - - -9.6916145086288452e-01 8.8583640754222870e-02 - <_> - - 0 -1 22 -7.9611856490373611e-03 - - 3.1446850299835205e-01 -2.9137840867042542e-01 - <_> - - 0 -1 69 1.2027498334646225e-02 - - -1.7159871757030487e-01 4.9999099969863892e-01 - <_> - - 0 -1 104 -3.6678132601082325e-03 - - 3.7841594219207764e-01 -3.0834850668907166e-01 - <_> - - 0 -1 124 -1.9784808158874512e-02 - - 5.4024320840835571e-01 -1.4204865694046021e-01 - <_> - - 0 -1 76 2.7927860617637634e-02 - - -2.1247063577175140e-01 4.3218085169792175e-01 - - <_> - 11 - -1.4899781942367554e+00 - - <_> - - 0 -1 130 7.8427735716104507e-03 - - -7.4531835317611694e-01 -3.0303031206130981e-02 - <_> - - 0 -1 42 -1.4624277129769325e-02 - - -1.6841350123286247e-02 -8.5118037462234497e-01 - <_> - - 0 -1 75 -3.8077287375926971e-02 - - 2.1741871535778046e-01 -3.6631068587303162e-01 - <_> - - 0 -1 12 -1.0661857202649117e-02 - - 4.3769642710685730e-01 -2.2932954132556915e-01 - <_> - - 0 -1 88 -1.3446703553199768e-02 - - -3.8489580154418945e-01 2.5192788243293762e-01 - <_> - - 0 -1 28 -1.4191937446594238e-01 - - -8.0028277635574341e-01 9.4658434391021729e-02 - <_> - - 0 -1 123 -3.4155612229369581e-04 - - -6.2979590892791748e-01 1.1930138617753983e-01 - <_> - - 0 -1 90 1.3760997913777828e-02 - - -1.4578124880790710e-01 5.2505373954772949e-01 - <_> - - 0 -1 66 -2.6961741968989372e-02 - - 4.6245655417442322e-01 -1.6671678423881531e-01 - <_> - - 0 -1 118 6.3529079779982567e-03 - - -2.1951504051685333e-01 3.5792681574821472e-01 - <_> - - 0 -1 72 -1.2992666102945805e-02 - - 7.6828622817993164e-01 -8.5931040346622467e-02 - - <_> - 17 - -1.4984009265899658e+00 - - <_> - - 0 -1 84 6.7299734801054001e-03 - - -7.6591378450393677e-01 -2.3893804848194122e-01 - <_> - - 0 -1 19 2.6990557089447975e-03 - - -3.9991316199302673e-01 1.5220387279987335e-01 - <_> - - 0 -1 16 -2.0344853401184082e-01 - - -7.5773400068283081e-01 9.2020586133003235e-02 - <_> - - 0 -1 122 -2.0129496988374740e-04 - - -4.4318243861198425e-01 2.0615091919898987e-01 - <_> - - 0 -1 5 -2.4270072579383850e-02 - - 3.9947441220283508e-01 -2.1658866107463837e-01 - <_> - - 0 -1 93 -6.8638771772384644e-03 - - 2.1516641974449158e-01 -3.4529885649681091e-01 - <_> - - 0 -1 68 -5.8059617877006531e-03 - - 3.8044607639312744e-01 -1.6675208508968353e-01 - <_> - - 0 -1 95 -9.3506037956103683e-05 - - 2.5903701782226563e-01 -2.8573530912399292e-01 - <_> - - 0 -1 62 -1.7116001248359680e-01 - - -9.1568988561630249e-01 6.8806290626525879e-02 - <_> - - 0 -1 58 1.2468735803849995e-04 - - -2.2247949242591858e-01 3.1490680575370789e-01 - <_> - - 0 -1 24 -1.0570101439952850e-03 - - -5.3656494617462158e-01 1.4280579984188080e-01 - <_> - - 0 -1 0 -1.0864594951272011e-02 - - 5.7343137264251709e-01 -1.9628804922103882e-01 - <_> - - 0 -1 102 -6.2631990294903517e-04 - - 3.2941296696662903e-01 -2.5868213176727295e-01 - <_> - - 0 -1 60 4.5934431254863739e-02 - - -2.3765510320663452e-01 3.2077547907829285e-01 - <_> - - 0 -1 15 5.2649816498160362e-03 - - -2.7179107069969177e-01 3.0700674653053284e-01 - <_> - - 0 -1 48 -1.0489258915185928e-02 - - 5.2368879318237305e-01 -1.7894229292869568e-01 - <_> - - 0 -1 46 -2.9591415077447891e-03 - - -5.0875836610794067e-01 1.5145763754844666e-01 - - <_> - 17 - -1.1793254613876343e+00 - - <_> - - 0 -1 31 4.8567064106464386e-02 - - -7.2802960872650146e-01 -1.0924369841814041e-01 - <_> - - 0 -1 87 -9.3583632260560989e-03 - - -7.0891864597797394e-02 -7.6105058193206787e-01 - <_> - - 0 -1 1 -2.6455814018845558e-02 - - 3.2114213705062866e-01 -1.8380424380302429e-01 - <_> - - 0 -1 100 7.3388908058404922e-03 - - -1.1695016920566559e-01 4.1009449958801270e-01 - <_> - - 0 -1 132 -7.4306890368461609e-02 - - -6.3738393783569336e-01 1.2330492585897446e-01 - <_> - - 0 -1 108 -1.1535363271832466e-02 - - -7.6230877637863159e-01 7.7160604298114777e-02 - <_> - - 0 -1 4 9.8390430212020874e-02 - - -1.3311645388603210e-01 5.7595914602279663e-01 - <_> - - 0 -1 32 -1.6651481389999390e-02 - - 2.5170418620109558e-01 -2.7978977560997009e-01 - <_> - - 0 -1 63 -8.9149046689271927e-03 - - 3.8843372464179993e-01 -1.9278995692729950e-01 - <_> - - 0 -1 111 -1.7397286137565970e-04 - - 3.8313332200050354e-01 -1.8763703107833862e-01 - <_> - - 0 -1 101 -1.1212332174181938e-03 - - -4.2161688208580017e-01 1.5696237981319427e-01 - <_> - - 0 -1 97 3.7472303956747055e-02 - - -1.4634302258491516e-01 4.6759790182113647e-01 - <_> - - 0 -1 125 3.9195418357849121e-03 - - -1.4772169291973114e-01 4.4500002264976501e-01 - <_> - - 0 -1 67 -6.3096638768911362e-03 - - -8.2744646072387695e-01 8.1156894564628601e-02 - <_> - - 0 -1 13 -1.0045848786830902e-01 - - 6.1077672243118286e-01 -1.2099583446979523e-01 - <_> - - 0 -1 92 -7.4745825259014964e-04 - - 3.7484991550445557e-01 -1.9619165360927582e-01 - <_> - - 0 -1 74 3.2814389560371637e-03 - - -1.8449757993221283e-01 3.7876024842262268e-01 - - <_> - - <_> - 0 0 19 2 -1. - <_> - 0 1 19 1 2. - 0 - <_> - - <_> - 0 1 6 6 -1. - <_> - 0 4 6 3 2. - 0 - <_> - - <_> - 0 2 2 14 -1. - <_> - 0 2 1 7 2. - <_> - 1 9 1 7 2. - 0 - <_> - - <_> - 0 4 6 11 -1. - <_> - 3 4 3 11 2. - 0 - <_> - - <_> - 0 4 10 20 -1. - <_> - 5 4 5 20 2. - 0 - <_> - - <_> - 0 8 4 11 -1. - <_> - 2 8 2 11 2. - 0 - <_> - - <_> - 0 10 2 9 -1. - <_> - 1 10 1 9 2. - 0 - <_> - - <_> - 0 10 8 12 -1. - <_> - 0 10 4 6 2. - <_> - 4 16 4 6 2. - 0 - <_> - - <_> - 0 10 24 12 -1. - <_> - 0 14 24 4 2. - 0 - <_> - - <_> - 0 11 2 7 -1. - <_> - 1 11 1 7 2. - 0 - <_> - - <_> - 0 12 2 10 -1. - <_> - 1 12 1 10 2. - 0 - <_> - - <_> - 0 14 4 9 -1. - <_> - 2 14 2 9 2. - 0 - <_> - - <_> - 0 19 6 3 -1. - <_> - 3 19 3 3 2. - 0 - <_> - - <_> - 1 0 3 18 -1. - <_> - 1 9 3 9 2. - 0 - <_> - - <_> - 1 1 13 4 -1. - <_> - 1 3 13 2 2. - 0 - <_> - - <_> - 1 2 10 4 -1. - <_> - 1 2 5 2 2. - <_> - 6 4 5 2 2. - 0 - <_> - - <_> - 1 2 20 22 -1. - <_> - 1 2 10 11 2. - <_> - 11 13 10 11 2. - 0 - <_> - - <_> - 1 5 4 7 -1. - <_> - 3 5 2 7 2. - 0 - <_> - - <_> - 1 6 4 2 -1. - <_> - 3 6 2 2 2. - 0 - <_> - - <_> - 1 11 4 3 -1. - <_> - 3 11 2 3 2. - 0 - <_> - - <_> - 1 16 8 6 -1. - <_> - 1 16 4 3 2. - <_> - 5 19 4 3 2. - 0 - <_> - - <_> - 1 16 22 4 -1. - <_> - 1 18 22 2 2. - 0 - <_> - - <_> - 1 21 1 3 -1. - <_> - 1 22 1 1 2. - 0 - <_> - - <_> - 2 0 1 3 -1. - <_> - 2 1 1 1 2. - 0 - <_> - - <_> - 2 0 3 2 -1. - <_> - 2 1 3 1 2. - 0 - <_> - - <_> - 2 0 3 12 -1. - <_> - 2 6 3 6 2. - 0 - <_> - - <_> - 2 0 12 6 -1. - <_> - 2 3 12 3 2. - 0 - <_> - - <_> - 2 5 4 6 -1. - <_> - 2 5 2 3 2. - <_> - 4 8 2 3 2. - 0 - <_> - - <_> - 2 5 10 16 -1. - <_> - 2 13 10 8 2. - 0 - <_> - - <_> - 2 16 18 6 -1. - <_> - 2 18 18 2 2. - 0 - <_> - - <_> - 3 0 4 8 -1. - <_> - 3 4 4 4 2. - 0 - <_> - - <_> - 3 0 6 10 -1. - <_> - 3 5 6 5 2. - 0 - <_> - - <_> - 3 13 14 8 -1. - <_> - 3 13 7 4 2. - <_> - 10 17 7 4 2. - 0 - <_> - - <_> - 3 20 3 2 -1. - <_> - 3 21 3 1 2. - 0 - <_> - - <_> - 3 22 8 2 -1. - <_> - 3 23 8 1 2. - 0 - <_> - - <_> - 3 22 13 2 -1. - <_> - 3 23 13 1 2. - 0 - <_> - - <_> - 4 0 1 3 -1. - <_> - 4 1 1 1 2. - 0 - <_> - - <_> - 4 1 13 2 -1. - <_> - 4 2 13 1 2. - 0 - <_> - - <_> - 5 0 9 6 -1. - <_> - 5 3 9 3 2. - 0 - <_> - - <_> - 5 1 12 4 -1. - <_> - 5 3 12 2 2. - 0 - <_> - - <_> - 5 3 2 4 -1. - <_> - 5 3 1 2 2. - <_> - 6 5 1 2 2. - 0 - <_> - - <_> - 5 6 2 6 -1. - <_> - 5 9 2 3 2. - 0 - <_> - - <_> - 5 10 1 12 -1. - <_> - 5 14 1 4 2. - 0 - <_> - - <_> - 5 13 2 6 -1. - <_> - 5 13 1 3 2. - <_> - 6 16 1 3 2. - 0 - <_> - - <_> - 5 20 13 2 -1. - <_> - 5 21 13 1 2. - 0 - <_> - - <_> - 5 22 16 2 -1. - <_> - 5 23 16 1 2. - 0 - <_> - - <_> - 6 0 8 1 -1. - <_> - 10 0 4 1 2. - 0 - <_> - - <_> - 6 1 16 4 -1. - <_> - 6 3 16 2 2. - 0 - <_> - - <_> - 6 2 6 4 -1. - <_> - 6 4 6 2 2. - 0 - <_> - - <_> - 6 22 10 2 -1. - <_> - 6 23 10 1 2. - 0 - <_> - - <_> - 7 0 12 3 -1. - <_> - 11 0 4 3 2. - 0 - <_> - - <_> - 7 1 14 4 -1. - <_> - 7 3 14 2 2. - 0 - <_> - - <_> - 7 2 2 4 -1. - <_> - 7 2 1 2 2. - <_> - 8 4 1 2 2. - 0 - <_> - - <_> - 7 6 15 10 -1. - <_> - 12 6 5 10 2. - 0 - <_> - - <_> - 7 7 5 3 -1. - <_> - 7 8 5 1 2. - 0 - <_> - - <_> - 8 0 9 4 -1. - <_> - 8 2 9 2 2. - 0 - <_> - - <_> - 8 3 3 2 -1. - <_> - 8 4 3 1 2. - 0 - <_> - - <_> - 8 5 8 6 -1. - <_> - 8 8 8 3 2. - 0 - <_> - - <_> - 8 7 1 2 -1. - <_> - 8 8 1 1 2. - 0 - <_> - - <_> - 8 9 9 4 -1. - <_> - 11 9 3 4 2. - 0 - <_> - - <_> - 8 20 14 4 -1. - <_> - 8 22 14 2 2. - 0 - <_> - - <_> - 9 3 6 4 -1. - <_> - 12 3 3 4 2. - 0 - <_> - - <_> - 9 3 14 21 -1. - <_> - 16 3 7 21 2. - 0 - <_> - - <_> - 9 5 12 4 -1. - <_> - 9 7 12 2 2. - 0 - <_> - - <_> - 9 15 4 1 -1. - <_> - 11 15 2 1 2. - 0 - <_> - - <_> - 10 0 3 6 -1. - <_> - 10 3 3 3 2. - 0 - <_> - - <_> - 10 0 11 4 -1. - <_> - 10 2 11 2 2. - 0 - <_> - - <_> - 10 4 1 6 -1. - <_> - 10 7 1 3 2. - 0 - <_> - - <_> - 10 4 8 2 -1. - <_> - 14 4 4 2 2. - 0 - <_> - - <_> - 10 5 14 1 -1. - <_> - 17 5 7 1 2. - 0 - <_> - - <_> - 10 8 5 14 -1. - <_> - 10 15 5 7 2. - 0 - <_> - - <_> - 10 16 3 6 -1. - <_> - 10 18 3 2 2. - 0 - <_> - - <_> - 10 20 5 4 -1. - <_> - 10 22 5 2 2. - 0 - <_> - - <_> - 10 20 12 4 -1. - <_> - 10 22 12 2 2. - 0 - <_> - - <_> - 11 6 8 1 -1. - <_> - 15 6 4 1 2. - 0 - <_> - - <_> - 11 8 6 14 -1. - <_> - 11 15 6 7 2. - 0 - <_> - - <_> - 11 13 8 10 -1. - <_> - 11 13 4 5 2. - <_> - 15 18 4 5 2. - 0 - <_> - - <_> - 11 19 4 2 -1. - <_> - 11 20 4 1 2. - 0 - <_> - - <_> - 11 19 5 2 -1. - <_> - 11 20 5 1 2. - 0 - <_> - - <_> - 11 22 12 2 -1. - <_> - 11 22 6 1 2. - <_> - 17 23 6 1 2. - 0 - <_> - - <_> - 12 2 9 2 -1. - <_> - 12 3 9 1 2. - 0 - <_> - - <_> - 12 3 4 3 -1. - <_> - 12 4 4 1 2. - 0 - <_> - - <_> - 12 5 3 1 -1. - <_> - 13 5 1 1 2. - 0 - <_> - - <_> - 12 10 5 12 -1. - <_> - 12 16 5 6 2. - 0 - <_> - - <_> - 12 22 3 2 -1. - <_> - 12 23 3 1 2. - 0 - <_> - - <_> - 13 6 4 10 -1. - <_> - 13 11 4 5 2. - 0 - <_> - - <_> - 13 7 3 5 -1. - <_> - 14 7 1 5 2. - 0 - <_> - - <_> - 13 8 2 3 -1. - <_> - 13 9 2 1 2. - 0 - <_> - - <_> - 13 10 3 2 -1. - <_> - 14 10 1 2 2. - 0 - <_> - - <_> - 13 20 8 4 -1. - <_> - 13 22 8 2 2. - 0 - <_> - - <_> - 14 0 7 4 -1. - <_> - 14 2 7 2 2. - 0 - <_> - - <_> - 14 2 4 2 -1. - <_> - 16 2 2 2 2. - 0 - <_> - - <_> - 14 5 2 4 -1. - <_> - 15 5 1 4 2. - 0 - <_> - - <_> - 14 5 4 10 -1. - <_> - 14 10 4 5 2. - 0 - <_> - - <_> - 14 8 2 10 -1. - <_> - 14 13 2 5 2. - 0 - <_> - - <_> - 14 20 3 2 -1. - <_> - 14 21 3 1 2. - 0 - <_> - - <_> - 15 5 2 10 -1. - <_> - 15 10 2 5 2. - 0 - <_> - - <_> - 16 2 8 8 -1. - <_> - 16 6 8 4 2. - 0 - <_> - - <_> - 16 18 1 6 -1. - <_> - 16 21 1 3 2. - 0 - <_> - - <_> - 16 18 3 6 -1. - <_> - 17 18 1 6 2. - 0 - <_> - - <_> - 16 21 4 2 -1. - <_> - 16 22 4 1 2. - 0 - <_> - - <_> - 16 23 8 1 -1. - <_> - 20 23 4 1 2. - 0 - <_> - - <_> - 17 2 2 5 -1. - <_> - 18 2 1 5 2. - 0 - <_> - - <_> - 17 5 7 6 -1. - <_> - 17 8 7 3 2. - 0 - <_> - - <_> - 17 7 6 2 -1. - <_> - 20 7 3 2 2. - 0 - <_> - - <_> - 17 18 3 6 -1. - <_> - 17 21 3 3 2. - 0 - <_> - - <_> - 17 23 6 1 -1. - <_> - 19 23 2 1 2. - 0 - <_> - - <_> - 18 0 1 10 -1. - <_> - 18 5 1 5 2. - 0 - <_> - - <_> - 18 0 6 4 -1. - <_> - 21 0 3 4 2. - 0 - <_> - - <_> - 18 13 3 3 -1. - <_> - 18 14 3 1 2. - 0 - <_> - - <_> - 18 15 6 9 -1. - <_> - 21 15 3 9 2. - 0 - <_> - - <_> - 18 18 2 1 -1. - <_> - 19 18 1 1 2. - 0 - <_> - - <_> - 18 23 6 1 -1. - <_> - 20 23 2 1 2. - 0 - <_> - - <_> - 19 9 5 3 -1. - <_> - 19 10 5 1 2. - 0 - <_> - - <_> - 20 0 4 17 -1. - <_> - 22 0 2 17 2. - 0 - <_> - - <_> - 20 4 4 14 -1. - <_> - 22 4 2 14 2. - 0 - <_> - - <_> - 20 7 4 6 -1. - <_> - 22 7 2 6 2. - 0 - <_> - - <_> - 20 7 4 8 -1. - <_> - 22 7 2 8 2. - 0 - <_> - - <_> - 20 8 2 14 -1. - <_> - 20 8 1 7 2. - <_> - 21 15 1 7 2. - 0 - <_> - - <_> - 20 9 2 12 -1. - <_> - 20 9 1 6 2. - <_> - 21 15 1 6 2. - 0 - <_> - - <_> - 20 11 4 10 -1. - <_> - 22 11 2 10 2. - 0 - <_> - - <_> - 20 16 2 6 -1. - <_> - 20 18 2 2 2. - 0 - <_> - - <_> - 22 0 1 2 -1. - <_> - 22 1 1 1 2. - 0 - <_> - - <_> - 22 0 2 2 -1. - <_> - 22 0 1 1 2. - <_> - 23 1 1 1 2. - 0 - <_> - - <_> - 22 4 1 18 -1. - <_> - 22 13 1 9 2. - 0 - <_> - - <_> - 22 6 2 3 -1. - <_> - 23 6 1 3 2. - 0 - <_> - - <_> - 22 6 2 6 -1. - <_> - 23 6 1 6 2. - 0 - <_> - - <_> - 22 7 2 12 -1. - <_> - 23 7 1 12 2. - 0 - <_> - - <_> - 22 8 2 7 -1. - <_> - 23 8 1 7 2. - 0 - <_> - - <_> - 22 8 2 12 -1. - <_> - 23 8 1 12 2. - 0 - <_> - - <_> - 22 11 2 3 -1. - <_> - 23 11 1 3 2. - 0 - <_> - - <_> - 23 1 1 6 -1. - <_> - 23 4 1 3 2. - 0 - <_> - - <_> - 23 3 1 21 -1. - <_> - 23 10 1 7 2. - 0 - <_> - - <_> - 23 4 1 9 -1. - <_> - 23 7 1 3 2. - 0 - <_> - - <_> - 23 16 1 6 -1. - <_> - 23 18 1 2 2. - 0 - diff --git a/009_bot/limestone_tooltip.jpg b/009_bot/limestone_tooltip.jpg deleted file mode 100644 index 089b718..0000000 Binary files a/009_bot/limestone_tooltip.jpg and /dev/null differ diff --git a/009_bot/main.py b/009_bot/main.py deleted file mode 100644 index 2f5e290..0000000 --- a/009_bot/main.py +++ /dev/null @@ -1,76 +0,0 @@ -import cv2 as cv -import numpy as np -import os -from time import time -from windowcapture import WindowCapture -from detection import Detection -from vision import Vision -from bot import AlbionBot, BotState - -# Change the working directory to the folder this script is in. -# Doing this because I'll be putting the files from each video in their -# own folder on GitHub -os.chdir(os.path.dirname(os.path.abspath(__file__))) - - -DEBUG = True - -# initialize the WindowCapture class -wincap = WindowCapture('Albion Online Client') -# load the detector -detector = Detection('limestone_model_final.xml') -# load an empty Vision class -vision = Vision() -# initialize the bot -bot = AlbionBot((wincap.offset_x, wincap.offset_y), (wincap.w, wincap.h)) - -wincap.start() -detector.start() -bot.start() - -while(True): - - # if we don't have a screenshot yet, don't run the code below this point yet - if wincap.screenshot is None: - continue - - # give detector the current screenshot to search for objects in - detector.update(wincap.screenshot) - - # update the bot with the data it needs right now - if bot.state == BotState.INITIALIZING: - # while bot is waiting to start, go ahead and start giving it some targets to work - # on right away when it does start - targets = vision.get_click_points(detector.rectangles) - bot.update_targets(targets) - elif bot.state == BotState.SEARCHING: - # when searching for something to click on next, the bot needs to know what the click - # points are for the current detection results. it also needs an updated screenshot - # to verify the hover tooltip once it has moved the mouse to that position - targets = vision.get_click_points(detector.rectangles) - bot.update_targets(targets) - bot.update_screenshot(wincap.screenshot) - elif bot.state == BotState.MOVING: - # when moving, we need fresh screenshots to determine when we've stopped moving - bot.update_screenshot(wincap.screenshot) - elif bot.state == BotState.MINING: - # nothing is needed while we wait for the mining to finish - pass - - if DEBUG: - # draw the detection results onto the original image - detection_image = vision.draw_rectangles(wincap.screenshot, detector.rectangles) - # display the images - cv.imshow('Matches', detection_image) - - # press 'q' with the output window focused to exit. - # waits 1 ms every loop to process key presses - key = cv.waitKey(1) - if key == ord('q'): - wincap.stop() - detector.stop() - bot.stop() - cv.destroyAllWindows() - break - -print('Done.') diff --git a/009_bot/vision.py b/009_bot/vision.py deleted file mode 100644 index 29992c8..0000000 --- a/009_bot/vision.py +++ /dev/null @@ -1,56 +0,0 @@ -import cv2 as cv -import numpy as np - - -class Vision: - - # given a list of [x, y, w, h] rectangles returned by find(), convert those into a list of - # [x, y] positions in the center of those rectangles where we can click on those found items - def get_click_points(self, rectangles): - points = [] - - # Loop over all the rectangles - for (x, y, w, h) in rectangles: - # Determine the center position - center_x = x + int(w/2) - center_y = y + int(h/2) - # Save the points - points.append((center_x, center_y)) - - return points - - # given a list of [x, y, w, h] rectangles and a canvas image to draw on, return an image with - # all of those rectangles drawn - def draw_rectangles(self, haystack_img, rectangles): - # these colors are actually BGR - line_color = (0, 255, 0) - line_type = cv.LINE_4 - - for (x, y, w, h) in rectangles: - # determine the box positions - top_left = (x, y) - bottom_right = (x + w, y + h) - # draw the box - cv.rectangle(haystack_img, top_left, bottom_right, line_color, lineType=line_type) - - return haystack_img - - # given a list of [x, y] positions and a canvas image to draw on, return an image with all - # of those click points drawn on as crosshairs - def draw_crosshairs(self, haystack_img, points): - # these colors are actually BGR - marker_color = (255, 0, 255) - marker_type = cv.MARKER_CROSS - - for (center_x, center_y) in points: - # draw the center point - cv.drawMarker(haystack_img, (center_x, center_y), marker_color, marker_type) - - return haystack_img - - def centeroid(self, point_list): - point_list = np.asarray(point_list, dtype=np.int32) - length = point_list.shape[0] - sum_x = np.sum(point_list[:, 0]) - sum_y = np.sum(point_list[:, 1]) - return [np.floor_divide(sum_x, length), np.floor_divide(sum_y, length)] diff --git a/009_bot/windowcapture.py b/009_bot/windowcapture.py deleted file mode 100644 index 148c331..0000000 --- a/009_bot/windowcapture.py +++ /dev/null @@ -1,126 +0,0 @@ -import numpy as np -import win32gui, win32ui, win32con -from threading import Thread, Lock - - -class WindowCapture: - - # threading properties - stopped = True - lock = None - screenshot = None - # properties - w = 0 - h = 0 - hwnd = None - cropped_x = 0 - cropped_y = 0 - offset_x = 0 - offset_y = 0 - - # constructor - def __init__(self, window_name=None): - # create a thread lock object - self.lock = Lock() - - # find the handle for the window we want to capture. - # if no window name is given, capture the entire screen - if window_name is None: - self.hwnd = win32gui.GetDesktopWindow() - else: - self.hwnd = win32gui.FindWindow(None, window_name) - if not self.hwnd: - raise Exception('Window not found: {}'.format(window_name)) - - # get the window size - window_rect = win32gui.GetWindowRect(self.hwnd) - self.w = window_rect[2] - window_rect[0] - self.h = window_rect[3] - window_rect[1] - - # account for the window border and titlebar and cut them off - border_pixels = 8 - titlebar_pixels = 30 - self.w = self.w - (border_pixels * 2) - self.h = self.h - titlebar_pixels - border_pixels - self.cropped_x = border_pixels - self.cropped_y = titlebar_pixels - - # set the cropped coordinates offset so we can translate screenshot - # images into actual screen positions - self.offset_x = window_rect[0] + self.cropped_x - self.offset_y = window_rect[1] + self.cropped_y - - def get_screenshot(self): - - # get the window image data - wDC = win32gui.GetWindowDC(self.hwnd) - dcObj = win32ui.CreateDCFromHandle(wDC) - cDC = dcObj.CreateCompatibleDC() - dataBitMap = win32ui.CreateBitmap() - dataBitMap.CreateCompatibleBitmap(dcObj, self.w, self.h) - cDC.SelectObject(dataBitMap) - cDC.BitBlt((0, 0), (self.w, self.h), dcObj, (self.cropped_x, self.cropped_y), win32con.SRCCOPY) - - # convert the raw data into a format opencv can read - #dataBitMap.SaveBitmapFile(cDC, 'debug.bmp') - signedIntsArray = dataBitMap.GetBitmapBits(True) - img = np.fromstring(signedIntsArray, dtype='uint8') - img.shape = (self.h, self.w, 4) - - # free resources - dcObj.DeleteDC() - cDC.DeleteDC() - win32gui.ReleaseDC(self.hwnd, wDC) - win32gui.DeleteObject(dataBitMap.GetHandle()) - - # drop the alpha channel, or cv.matchTemplate() will throw an error like: - # error: (-215:Assertion failed) (depth == CV_8U || depth == CV_32F) && type == _templ.type() - # && _img.dims() <= 2 in function 'cv::matchTemplate' - img = img[...,:3] - - # make image C_CONTIGUOUS to avoid errors that look like: - # File ... in draw_rectangles - # TypeError: an integer is required (got type tuple) - # see the discussion here: - # https://github.com/opencv/opencv/issues/14866#issuecomment-580207109 - img = np.ascontiguousarray(img) - - return img - - # find the name of the window you're interested in. - # once you have it, update window_capture() - # https://stackoverflow.com/questions/55547940/how-to-get-a-list-of-the-name-of-every-open-window - @staticmethod - def list_window_names(): - def winEnumHandler(hwnd, ctx): - if win32gui.IsWindowVisible(hwnd): - print(hex(hwnd), win32gui.GetWindowText(hwnd)) - win32gui.EnumWindows(winEnumHandler, None) - - # translate a pixel position on a screenshot image to a pixel position on the screen. - # pos = (x, y) - # WARNING: if you move the window being captured after execution is started, this will - # return incorrect coordinates, because the window position is only calculated in - # the __init__ constructor. - def get_screen_position(self, pos): - return (pos[0] + self.offset_x, pos[1] + self.offset_y) - - # threading methods - - def start(self): - self.stopped = False - t = Thread(target=self.run) - t.start() - - def stop(self): - self.stopped = True - - def run(self): - # TODO: you can write your own time/iterations calculation to determine how fast this is - while not self.stopped: - # get an updated image of the game - screenshot = self.get_screenshot() - # lock the thread while updating the results - self.lock.acquire() - self.screenshot = screenshot - self.lock.release() diff --git a/kha_lastz_auto/DOCS.md b/kha_lastz_auto/DOCS.md new file mode 100644 index 0000000..73f67a7 --- /dev/null +++ b/kha_lastz_auto/DOCS.md @@ -0,0 +1,424 @@ +# kha_lastz_auto — Documentation + +An automation bot for the game LastZ. Uses OpenCV template matching to detect and interact with UI elements. + +--- + +## Project Structure + +``` +kha_lastz_auto/ +├── main.py # Entry point — main loop, hotkey listener, cron scheduler +├── bot_engine.py # Loads and executes functions, handles each step type +├── attack_detector.py # Detects being-attacked state via icon template matching +├── vision.py # Template matching wrapper (OpenCV) +├── windowcapture.py # Captures screenshots from the game window +├── config.yaml # Main config: window size, function list +├── functions/ # Each .yaml file defines one automation function +├── buttons_template/ # Template images used for screen matching +└── .env # Secrets (e.g. PIN_PASSWORD=1234) +``` + +--- + +## config.yaml + +```yaml +reference_width: 2400 +reference_height: 1600 + +functions: + - name: FightBoomer + key: b + cron: "*/2 * * * *" + priority: 2 + enabled: true +``` + +| Field | Description | +|---|---| +| `reference_width/height` | Client area size of the game window when templates were captured. The bot resizes the window to this size on startup so scale = 1.0 and matching is fast. | +| `name` | Function name — must match the filename in `functions/` (without `.yaml`). | +| `key` | Hotkey to toggle the function on/off. Press the same key while running to stop. | +| `cron` | Cron expression for automatic scheduling. See [crontab.guru](https://crontab.guru/). | +| `priority` | Lower number = higher priority. A higher-priority function can preempt a running one. | +| `trigger` | Special event trigger. Currently supports `attacked` — fires the function when attack is detected. Cannot be combined with `cron`. | +| `enabled` | `true/false` — disables the function from both cron and hotkey when `false`. | + +### Priority & Scheduler + +When a function is triggered (hotkey or cron) while another is already running: + +- **Higher priority** → preempts immediately; the current function is stopped. +- **Lower priority** → added to the queue; runs after the current function finishes. +- **Same priority** → stops the current function and starts the new one. + +After any function finishes, the queue is drained in priority order. + +### Controls + +| Input | Action | +|---|---| +| Hotkey (e.g. `b`) | Start function / press again to stop | +| `Ctrl + Esc` | Quit the bot cleanly | +| `q` on preview window | Quit the bot | + +### Trigger: attacked + +A function with `trigger: attacked` starts automatically the moment an attack is detected — no hotkey or cron needed. + +```yaml + - name: TurnOnShield + trigger: attacked + priority: 1 # high priority so it preempts other running functions + enabled: true +``` + +Multiple functions can use `trigger: attacked`. They are all queued/started using the same priority rules as cron and hotkey triggers. + +--- + +## Functions + +Each function is a YAML file in `functions/` containing a `description` and a list of `steps`. + +Steps run **sequentially**. If a step returns `false` (not found within timeout), all remaining steps are **skipped** (function aborted), unless a step has `run_always: true`. + +### Available Functions + +| Function | Description | +|---|---| +| `PinLoggin` | Auto-login when the PIN entry screen appears | +| `FightBoomer` | Open Magnifier → select Boomer → set level → Search → Team Up → March | +| `ClickTreasure` | Continuously click the Treasure Helicopter when visible | +| `CollectExplorationReward` | Zoom out → HQ → ExplorationReward → Claim → Collect | +| `SoliderTrain` | Zoom out → HQ → train Shooter / Assault / Rider soldiers | +| `HelpAlliance` | Click the Help Alliance button when visible | +| `CheckMail` | Open Mail → Alliance tab → Claim All → System tab → Claim All → close | +| `DonateAllianceTech` | Open Alliance → Techs → Like → Donate x20 | + +--- + +## Event Types + +### `match_click` +Find a template on screen and click it. + +```yaml +- event_type: match_click + template: buttons_template/MyButton.png + threshold: 0.75 # match confidence (0.0–1.0), default 0.75 + one_shot: true # true = click once then advance + # false = keep clicking until max_clicks or timeout + timeout_sec: 10 # seconds to wait before giving up + max_clicks: 20 # (one_shot: false) maximum number of clicks + click_interval_sec: 0.15 # (one_shot: false) pause between clicks + click_offset_x: 0.5 # shift click right by N × template_width (e.g. 0.5 = half width) + click_offset_y: 0.0 # shift click down by N × template_height (negative = up) + click_random_offset: 20 # randomize click position ±N pixels (anti-bot detection) + run_always: false # true = run even if a previous step returned false + debug_click: false # true = save a debug PNG showing match area and click position +``` + +Returns `true` on successful click, `false` when `timeout_sec` expires without a match. + +#### `debug_click` + +When `debug_click: true`, saves a debug PNG to the `kha_lastz_auto/` folder after the step runs: + +- **Green rectangle** — the matched template area on screen +- **Red circle** — the exact position the cursor will click / move to (after applying offsets) + +File name format: `debug__.png` + +| Example | Output file | +|---|---| +| `match_click` + `buttons_template/Use8HoursPeaceShield.png` | `debug_match_click_Use8HoursPeaceShield.png` | +| `match_move` + `buttons_template/PeaceShield.png` | `debug_match_move_PeaceShield.png` | + +Useful when calibrating `click_offset_x` / `click_offset_y` values to verify the click lands exactly where intended. + +--- + +#### `click_offset_x` / `click_offset_y` + +Shift the click position relative to the center of the matched template, expressed as a **ratio of the template size**. + +```yaml +click_offset_x: 0.5 # shift right by 50% of template width +click_offset_y: -0.5 # shift up by 50% of template height +``` + +| Value | Effect | +|---|---| +| `0.0` (default) | click the template center | +| `0.5` | shift right / down by half the template dimension | +| `-0.5` | shift left / up by half the template dimension | + +Applied before `click_random_offset`. + +--- + +### `match_move` +Find a template on screen and move the mouse to it — no click. + +```yaml +- event_type: match_move + template: buttons_template/MyTarget.png + threshold: 0.75 + timeout_sec: 10 + click_offset_x: 0.0 # same offset rules as match_click + click_offset_y: 0.0 + debug_click: false # true = save a debug PNG showing match area and move target +``` + +Returns `true` when the mouse is moved successfully, `false` on timeout. + +--- + +### `match_multi_click` +Find **all** visible instances of a template and click each one. + +```yaml +- event_type: match_multi_click + template: buttons_template/MyButton.png + threshold: 0.75 + timeout_sec: 10 + click_interval_sec: 0.15 +``` + +Returns `true` after clicking all matches. If none found within `timeout_sec`, still returns `true` (does not abort). + +--- + +### `match_count` +Check how many times a template appears on screen. Does not click. + +```yaml +- event_type: match_count + template: buttons_template/PasswordSlot.png + count: 6 # require at least N instances + threshold: 0.75 + timeout_sec: 8 + debug_save: false # true = save screenshot to debug/ on failure +``` + +Returns `true` when at least `count` instances are found. Returns `false` on timeout. + +--- + +### `wait_until_match` +Wait until a template appears on screen. Does not click. + +```yaml +- event_type: wait_until_match + template: buttons_template/LoadingDone.png + threshold: 0.75 + timeout_sec: 30 +``` + +Returns `true` when found, `false` on timeout. + +--- + +### `click_unless_visible` +If `visible_template` is **already on screen** → skip (already on the right screen). +If **not found** → click `click_template` to navigate there. + +```yaml +- event_type: click_unless_visible + visible_template: buttons_template/MagnifierButton.png + click_template: buttons_template/WorldButton.png + threshold: 0.75 + timeout_sec: 3 +``` + +Always returns `true`. + +--- + +### `sleep` +Wait for a fixed duration. + +```yaml +- event_type: sleep + duration_sec: 1.5 + run_always: false +``` + +--- + +### `click_position` +Click a fixed position defined as a ratio of the window size. + +```yaml +- event_type: click_position + offset_x: 0.15 # 15% from the left edge + offset_y: 0.15 # 15% from the top edge + run_always: false +``` + +Commonly used to close popups or dismiss banners by clicking an empty corner. + +--- + +### `close_ui` (special) +Closes popups / overlays: repeatedly **clicks a fixed position** (default top-left corner) until **Headquarters** or **World** is found on screen (or max tries reached). Use **before** `base_zoomout` to ensure the base/world view is visible before zooming out. + +```yaml +- event_type: close_ui + template: buttons_template/HeadquartersButton.png # HQ template (required) + world_button: buttons_template/WorldButton.png # World template (optional) + threshold: 0.75 + click_x: 0.03 # X position ratio (default 0.03 = near left edge) + click_y: 0.08 # Y position ratio (default 0.08 = near top edge) + max_tries: 10 # max click-then-capture cycles, default 10 + roi_center_x: 0.93 # (optional) search HQ/World only within this ROI + roi_center_y: 0.96 + roi_padding: 2 # multiplier of template size to define ROI + debug_log: false +``` + +**Flow:** Each iteration: search for HQ and World (in ROI if set). If **at least one** is found → exit and advance step. Otherwise → click at `(click_x, click_y)` → sleep 1s → capture new screenshot → repeat. After exit (or `max_tries`), step always advances and returns success. + +--- + +### `base_zoomout` (special) +Zooms the map out to the world view: finds **Headquarters** or **World**, clicks in sequence (HQ → re-check World/HQ → scroll to zoom out). Use **after** `close_ui`. + +```yaml +- event_type: base_zoomout + template: buttons_template/HeadquartersButton.png + world_button: buttons_template/WorldButton.png # optional + threshold: 0.75 + scroll_times: 5 + scroll_interval_sec: 0.1 + timeout_sec: 5 + roi_center_x: 0.93 # (optional) search HQ/World only within this ROI + roi_center_y: 0.96 + roi_padding: 2 + debug_log: false + debug_save: false # true = save ROI image when HQ/World not found (for debugging) +``` + +**Flow:** (1) If HQ found → click HQ → capture again; if World then visible → scroll zoom out → advance. If HQ still visible → click HQ once more then scroll. (2) If World found from the start → click World → retry step (next run finds HQ, clicks HQ, zooms out). (3) If neither HQ nor World found → still scroll zoom out then advance. Always returns success. + +--- + +### `world_zoomout` (special) +Opposite of `base_zoomout`: zoom out when already on the **world map**. Requires both `template` and `world_button` (same as base_zoomout). "On world" = the given template (e.g. HeadquartersButton) is visible. Only scrolls when the template is found; otherwise retries until `timeout_sec` (then aborts the function). + +```yaml +- event_type: world_zoomout + template: buttons_template/HeadquartersButton.png # required; when visible = we're on world + world_button: buttons_template/WorldButton.png # required (same as base_zoomout) + threshold: 0.75 + scroll_times: 5 + scroll_interval_sec: 0.1 + timeout_sec: 15 # if template not found for this long, abort step (avoid infinite retry) + roi_center_x: 0.93 # optional: search only in this region + roi_center_y: 0.96 + roi_padding: 2 + debug_log: false + debug_save: false # true = save ROI image when template not found +``` + +**Flow:** Search for `template` (e.g. HeadquartersButton) in the screenshot (in ROI if set). If found → scroll zoom out at center, advance step, return success. If not found but `world_button` (WorldButton) is found → we're on base; click WorldButton, sleep 2s, retry (next frame we're on world and can scroll). If neither found → retry; after `timeout_sec` seconds, abort the step (function stops). + +--- + +### `key_press` +Press a keyboard key. + +```yaml +- event_type: key_press + key: escape +``` + +--- + +### `type_text` +Type a string. Supports `.env` variable substitution via `${VAR_NAME}`. + +```yaml +- event_type: type_text + text: "${PIN_PASSWORD}" + interval_sec: 0.1 # delay between keystrokes in seconds +``` + +--- + +### `set_level` +Use OCR to read the current level, then click Plus/Minus until `target_level` is reached. + +```yaml +- event_type: set_level + target_level: 10 + level_anchor_template: buttons_template/Slider.png # template used as position anchor + level_anchor_offset: [-45, -65, 110, 35] # [dx, dy, w, h] from anchor center + plus_template: buttons_template/PlusButton.png + minus_template: buttons_template/MinusButton.png + threshold: 0.75 + timeout_sec: 20 + debug_save_roi: true # save the first OCR crop to a file for debugging +``` + +Requires Tesseract OCR to be installed. + +--- + +## Attack Detector + +`attack_detector.py` detects when the player's base is under attack by matching the `BeingAttackedWarning` icon on screen. + +- **Attack starts**: icon found in any single frame → logs `[Alert] House is being attacked!` +- **Attack ends**: icon absent continuously for `clear_sec` seconds → logs `[Alert] Attack has ended.` + +Configured in `main.py`: + +```python +attack_detector = AttackDetector( + warning_template_path="buttons_template/BeingAttackedWarning.png", + clear_sec=10.0, # seconds of no icon before declaring attack over +) +``` + +--- + +## Adding a New Function + +1. Create `functions/MyFunction.yaml`: + +```yaml +description: "Short description of what this function does" +steps: + - event_type: match_click + template: buttons_template/SomeButton.png + threshold: 0.75 + one_shot: true + timeout_sec: 10 +``` + +2. Register it in `config.yaml`: + +```yaml + - name: MyFunction + key: x + cron: "*/5 * * * *" + priority: 50 + enabled: true +``` + +3. Add template images to `buttons_template/` — crop them from the game window at exactly `reference_width x reference_height` resolution. + +--- + +## .env + +Stores secrets. Never commit this file. + +```env +PIN_PASSWORD=1234 +``` + +Reference in YAML steps with `${PIN_PASSWORD}`. diff --git a/kha_lastz_auto/adb_emulator_context.py b/kha_lastz_auto/adb_emulator_context.py new file mode 100644 index 0000000..d278fd2 --- /dev/null +++ b/kha_lastz_auto/adb_emulator_context.py @@ -0,0 +1,82 @@ +""" +ADB-only game surface context for LDPlayer mode. + +Provides the same duck-typed fields used by bot steps (``w``, ``h``, ``offset_*``, +``get_screen_position``, ``get_screenshot``) in **device pixel** space — matching +``adb exec-out screencap`` and ``adb shell input tap``. + +Optional ``[MOUSE-LOG]`` for ROI tuning uses **only ADB** (``getevent`` touch +coordinates on the device), not Win32 or pynput. +""" + +from __future__ import annotations + +import logging +from typing import Any, Tuple + +log = logging.getLogger("kha_lastz") + + +class AdbEmulatorContext: + """ + Bounds and helpers for the emulated Android display via ADB only. + + ``get_screen_position`` returns its argument unchanged (device pixels); legacy + callers use that name for Win32 screen mapping on PC mode. + """ + + is_using_adb = True + hwnd = None + auto_focus = False + offset_x = 0 + offset_y = 0 + cropped_x = 0 + cropped_y = 0 + + def __init__(self, capture_service: Any, enable_mouse_log: bool = True) -> None: + """``capture_service`` is ScreenshotCaptureService (shared cache; no direct ADB grab here).""" + self._capture_service = capture_service + self.w = 0 + self.h = 0 + if enable_mouse_log: + try: + import adb_input as adb_mod + + adb = adb_mod.get_adb_input() + if adb is not None: + adb.start_getevent_mouse_log() + except Exception as exc: + log.warning("[AdbEmulatorContext] MOUSE-LOG (getevent) not started: %s", exc) + + def refresh_geometry(self) -> None: + """Refresh ``w`` / ``h`` from ``wm size`` or the last screencap shape.""" + import adb_input as adb_mod + + adb = adb_mod.get_adb_input() + if adb is not None: + sz = adb.get_device_screen_size() + if sz: + self.w, self.h = int(sz[0]), int(sz[1]) + return + img = self._capture_service.get_cached() if self._capture_service else None + if img is not None and getattr(img, "shape", None) is not None and len(img.shape) >= 2: + self.h, self.w = int(img.shape[0]), int(img.shape[1]) + + def get_screen_position(self, pos: Tuple[int, int]) -> Tuple[int, int]: + """Pass through device (screenshot) pixel coordinates.""" + return (int(pos[0]), int(pos[1])) + + def get_screenshot(self): + """Last cached frame from ScreenshotCaptureService (no extra screencap).""" + return self._capture_service.get_cached() if self._capture_service else None + + def focus_window(self, force: bool = False) -> None: + """No-op: there is no host window to focus in pure ADB mode.""" + + def resize_to_client(self, target_w: int, target_h: int) -> bool: + log.debug( + "[AdbEmulatorContext] resize_to_client(%s,%s) ignored — set resolution in LDPlayer.", + target_w, + target_h, + ) + return False diff --git a/kha_lastz_auto/adb_input.py b/kha_lastz_auto/adb_input.py new file mode 100644 index 0000000..4c49f87 --- /dev/null +++ b/kha_lastz_auto/adb_input.py @@ -0,0 +1,506 @@ +""" +adb_input.py +------------ +ADB-based input (tap / swipe) for LDPlayer emulator mode. + +Coordinates are always in **device (client) pixels** — the same coordinate +space as the game screenshot — NOT Windows screen pixels. + +Commands issued: + tap: adb shell input tap + swipe: adb shell input swipe [duration_ms] + +Touch coordinates for ``[MOUSE-LOG]`` (LDPlayer ROI tuning) come from +``adb shell getevent -lt`` only — no Win32 / pynput on the host. + +Auto-detect flow (called by main.py on startup): + 1. Run ``adb devices`` — use the first online device found. + 2. If none, try ``adb connect 127.0.0.1:{port}`` for LDPlayer's known + default ports (5555, 5557, 5559 … one per emulator instance). + 3. If a manual ``ldplayer_device_serial`` is set in config.yaml, + skip auto-detect entirely and use that serial directly. + +Usage (main.py sets up the singleton; event handlers read it): + + # main.py + import adb_input + inst = adb_input.AdbInput(adb_path=...) + inst.detect_and_connect() # auto-detect LDPlayer + adb_input.set_adb_input(inst) + + # event handler + import adb_input + _adb = adb_input.get_adb_input() + if _adb is not None: + _adb.tap(cx, cy) + else: + # fall back to win32 / pyautogui +""" + +import logging +import re +import subprocess +import threading +import time +from typing import List, Optional, Tuple + +log = logging.getLogger("kha_lastz") + +_DEFAULT_ADB_PATH = r"C:\LDPlayer\LDPlayer9\adb.exe" +_ADB_TIMEOUT_SEC = 5 + +# LDPlayer assigns one TCP port per emulator instance starting at 5555, +# incrementing by 2 (5555, 5557, 5559 …). We probe the first 8 slots. +_LDPLAYER_ADB_PORTS: List[int] = [5555, 5557, 5559, 5561, 5563, 5565, 5567, 5569] + +# Parsed from ``adb shell getevent -lt`` (multitouch or single-touch ABS). +_RE_GETEVENT_MT_X = re.compile(r"ABS_MT_POSITION_X\s+([0-9a-fA-F]+)") +_RE_GETEVENT_MT_Y = re.compile(r"ABS_MT_POSITION_Y\s+([0-9a-fA-F]+)") +_RE_GETEVENT_ABS_X = re.compile(r"\bABS_X\s+([0-9a-fA-F]+)") +_RE_GETEVENT_ABS_Y = re.compile(r"\bABS_Y\s+([0-9a-fA-F]+)") + + +# ── Low-level helpers ────────────────────────────────────────────────────────── + +def _run_adb_text(adb_path: str, args: List[str], timeout: int = 5) -> Optional[str]: + """Run an adb command and return decoded stdout, or None on any error.""" + try: + result = subprocess.run( + [adb_path] + args, + capture_output=True, + text=True, + timeout=timeout, + ) + return result.stdout + except Exception: + return None + + +def _parse_adb_devices(output: str) -> List[str]: + """ + Parse ``adb devices`` output and return serials of online (ready) devices. + + Skips devices that are ``offline``, ``unauthorized``, or ``no permissions``. + """ + serials: List[str] = [] + for line in output.splitlines(): + line = line.strip() + if not line or line.startswith("List of") or line.startswith("*"): + continue + parts = line.split("\t") + if len(parts) >= 2 and parts[1].strip() == "device": + serials.append(parts[0].strip()) + return serials + + +# ── Public auto-detect function ──────────────────────────────────────────────── + +def detect_ldplayer_device(adb_path: Optional[str] = None) -> Optional[str]: + """ + Auto-detect a running LDPlayer ADB device and return its serial. + + Detection steps + --------------- + 1. ``adb devices`` — return the first online serial immediately. + 2. If none found, iterate through ``_LDPLAYER_ADB_PORTS`` and try + ``adb connect 127.0.0.1:{port}``. Return the first successful address. + + Returns + ------- + str Device serial such as ``"emulator-5554"`` or ``"127.0.0.1:5555"``, + or ``None`` when nothing is found. + """ + path = adb_path or _DEFAULT_ADB_PATH + + # ── Step 1: already-connected devices ───────────────────────────────────── + out = _run_adb_text(path, ["devices"]) + if out: + serials = _parse_adb_devices(out) + if serials: + log.info( + "[ADB] Auto-detect: found connected device(s): %s — using '%s'", + serials, serials[0], + ) + return serials[0] + + # ── Step 2: probe LDPlayer TCP ports ────────────────────────────────────── + log.info( + "[ADB] Auto-detect: no device in 'adb devices', " + "probing LDPlayer ports %s …", + _LDPLAYER_ADB_PORTS, + ) + for port in _LDPLAYER_ADB_PORTS: + addr = "127.0.0.1:{}".format(port) + out = _run_adb_text(path, ["connect", addr], timeout=3) + if out and ("connected" in out.lower() or "already connected" in out.lower()): + log.info("[ADB] Auto-detect: connected to LDPlayer at %s", addr) + return addr + log.debug("[ADB] Auto-detect: port %d → %s", port, (out or "").strip()) + + log.warning("[ADB] Auto-detect: no LDPlayer device found on any known port.") + return None + + +def parse_wm_size_output(stdout: str) -> Optional[Tuple[int, int]]: + """ + Parse ``adb shell wm size`` text and return (width, height) in device pixels. + + Prefer ``Override size`` when present (logical resolution used by input and + screencap); otherwise use ``Physical size``. + """ + if not stdout: + return None + override_wh: Optional[Tuple[int, int]] = None + physical_wh: Optional[Tuple[int, int]] = None + for line in stdout.splitlines(): + line_l = line.strip() + m = re.search(r"(\d+)\s*x\s*(\d+)", line_l) + if not m: + continue + w, h = int(m.group(1)), int(m.group(2)) + if "override" in line_l.lower(): + override_wh = (w, h) + elif "physical" in line_l.lower(): + physical_wh = (w, h) + if override_wh: + return override_wh + if physical_wh: + return physical_wh + m = re.search(r"(\d+)\s*x\s*(\d+)", stdout) + if m: + return int(m.group(1)), int(m.group(2)) + return None + + +def map_ldplayer_client_to_device( + client_x: float, + client_y: float, + client_w: int, + client_h: int, + device_w: int, + device_h: int, +) -> Optional[Tuple[int, int]]: + """ + Map LDPlayer **Win32 client** coordinates to ADB device pixels. + + The emulator draws the Android framebuffer with uniform scaling inside the + client; extra space from aspect mismatch is assumed at the **top** (toolbar / + letterbox), matching typical LDPlayer layout — viewport is bottom-aligned. + + Returns + ------- + (dx, dy) clamped to the device rectangle, or ``None`` if the point lies + outside the emulated viewport (e.g. title toolbar strip). + """ + if client_w <= 0 or client_h <= 0 or device_w <= 0 or device_h <= 0: + return None + scale = min(client_w / device_w, client_h / device_h) + vw = device_w * scale + vh = device_h * scale + off_x = (client_w - vw) * 0.5 + off_y = client_h - vh + if not (off_x <= client_x < off_x + vw and off_y <= client_y < off_y + vh): + return None + nx = client_x - off_x + ny = client_y - off_y + dx = int(round(nx / scale)) + dy = int(round(ny / scale)) + dx = max(0, min(device_w - 1, dx)) + dy = max(0, min(device_h - 1, dy)) + return dx, dy + + +# ── AdbInput class ───────────────────────────────────────────────────────────── + +class AdbInput: + """ + Sends touch-input commands to a connected ADB device (LDPlayer emulator). + + All coordinates are in device (client) pixels — the same space as + OpenCV screenshots, not Windows screen coordinates. + """ + + def __init__( + self, + adb_path: Optional[str] = None, + device_serial: Optional[str] = None, + ) -> None: + self._adb_path = adb_path or _DEFAULT_ADB_PATH + self._device_serial = device_serial + self._wm_size_cache: Optional[Tuple[int, int, float]] = None + self._wm_size_cache_ttl_sec = 10.0 + self._getevent_log_thread: Optional[threading.Thread] = None + self._getevent_proc: Optional[subprocess.Popen] = None + self._mouse_log_debounce_xy: Optional[Tuple[int, int]] = None + self._mouse_log_debounce_t: float = 0.0 + + def _shell_text(self, *shell_args: str) -> Optional[str]: + """Run ``adb shell ...`` and return stdout text, or None on failure.""" + args: List[str] = [] + if self._device_serial: + args += ["-s", self._device_serial] + args += ["shell"] + list(shell_args) + return _run_adb_text(self._adb_path, args) + + def get_device_screen_size(self, refresh: bool = False) -> Optional[Tuple[int, int]]: + """ + Return ``(width, height)`` from ``wm size`` (same space as screencap / input tap). + + Results are cached briefly to avoid spawning adb on every mouse click. + """ + now = time.monotonic() + if ( + not refresh + and self._wm_size_cache is not None + and now - self._wm_size_cache[2] < self._wm_size_cache_ttl_sec + ): + return self._wm_size_cache[0], self._wm_size_cache[1] + out = self._shell_text("wm", "size") + parsed = parse_wm_size_output(out or "") + if parsed: + w, h = parsed + self._wm_size_cache = (w, h, now) + return w, h + return None + + # ── Device detection ─────────────────────────────────────────────────────── + + def detect_and_connect(self) -> Optional[str]: + """ + Auto-detect a running LDPlayer instance and store its serial. + + Calls :func:`detect_ldplayer_device` and, on success, updates + ``self._device_serial`` so all subsequent ``tap`` / ``swipe`` calls + target the detected device. + + Returns the serial string, or ``None`` if nothing was found. + """ + serial = detect_ldplayer_device(self._adb_path) + if serial: + self._device_serial = serial + log.info("[ADB Input] Device serial: %s", serial) + else: + log.warning( + "[ADB Input] No LDPlayer device detected. " + "Set ldplayer_device_serial in config.yaml to specify manually." + ) + return serial + + # ── Touch → MOUSE-LOG (getevent, no host Win32) ───────────────────────────── + + def start_getevent_mouse_log(self) -> None: + """ + Background thread: ``adb shell getevent -lt`` → print ``[MOUSE-LOG]`` on each + touch report using **device** coordinates (same space as screencap / ``input tap``). + """ + if self._getevent_log_thread is not None and self._getevent_log_thread.is_alive(): + return + self._getevent_log_thread = threading.Thread( + target=self._getevent_mouse_log_loop, + name="adb-getevent-mouselog", + daemon=True, + ) + self._getevent_log_thread.start() + log.info("[ADB] MOUSE-LOG: listening via `getevent -lt` (device touch coordinates).") + + def _emit_mouse_log_line(self, dx: int, dy: int) -> None: + """Debounce and print the same block as PC ``WindowCapture`` mouse debug.""" + now = time.monotonic() + if self._mouse_log_debounce_xy == (dx, dy) and now - self._mouse_log_debounce_t < 0.08: + return + self._mouse_log_debounce_xy = (dx, dy) + self._mouse_log_debounce_t = now + + dev = self.get_device_screen_size() + if not dev: + return + dw, dh = dev + if dw <= 0 or dh <= 0: + return + rel_x = dx / dw + rel_y = dy / dh + if not (0 <= rel_x <= 1 and 0 <= rel_y <= 1): + return + + print(f"\n[MOUSE-LOG] Inside Game Window:") + print(f" - Local Pixel: ({dx}, {dy})") + print(f" - Ratio: x={rel_x:.4f}, y={rel_y:.4f}") + print(f" - YAML ROI (copy this):") + print(f" roi_center_x: {rel_x:.2f}") + print(f" roi_center_y: {rel_y:.2f}") + + def _getevent_mouse_log_loop(self) -> None: + cmd = [self._adb_path] + if self._device_serial: + cmd += ["-s", self._device_serial] + cmd += ["shell", "getevent", "-lt"] + try: + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + bufsize=1, + ) + except Exception as exc: + log.warning("[ADB] MOUSE-LOG: could not spawn getevent: %s", exc) + return + + self._getevent_proc = proc + mt_x: Optional[int] = None + mt_y: Optional[int] = None + abs_x: Optional[int] = None + abs_y: Optional[int] = None + + try: + if proc.stdout is None: + return + for raw in proc.stdout: + line = raw.strip() + m = _RE_GETEVENT_MT_X.search(line) + if m: + mt_x = int(m.group(1), 16) + m = _RE_GETEVENT_MT_Y.search(line) + if m: + mt_y = int(m.group(1), 16) + m = _RE_GETEVENT_ABS_X.search(line) + if m: + abs_x = int(m.group(1), 16) + m = _RE_GETEVENT_ABS_Y.search(line) + if m: + abs_y = int(m.group(1), 16) + + if "SYN_REPORT" not in line: + continue + + dx: Optional[int] = None + dy: Optional[int] = None + if mt_x is not None and mt_y is not None: + dx, dy = mt_x, mt_y + elif abs_x is not None and abs_y is not None: + dx, dy = abs_x, abs_y + + mt_x = mt_y = None + abs_x = abs_y = None + + if dx is not None and dy is not None: + self._emit_mouse_log_line(dx, dy) + except Exception as exc: + log.debug("[ADB] getevent MOUSE-LOG loop exited: %s", exc) + finally: + try: + proc.kill() + except Exception: + pass + self._getevent_proc = None + + # ── Internal command runner ──────────────────────────────────────────────── + + def _run(self, args: List[str]) -> bool: + """Execute an adb command. Returns True on success.""" + cmd = [self._adb_path] + if self._device_serial: + cmd += ["-s", self._device_serial] + cmd += args + try: + result = subprocess.run( + cmd, capture_output=True, timeout=_ADB_TIMEOUT_SEC + ) + if result.returncode != 0: + log.warning( + "[ADB] command failed (rc=%d): %s", + result.returncode, + " ".join(args), + ) + return False + return True + except subprocess.TimeoutExpired: + log.warning("[ADB] command timed out: %s", " ".join(args)) + return False + except FileNotFoundError: + log.error( + "[ADB] adb not found at '%s'. " + "Check ldplayer_adb_path in config.yaml or install ADB.", + self._adb_path, + ) + return False + except Exception as exc: + log.warning("[ADB] command error: %s", exc) + return False + + # ── Input commands ───────────────────────────────────────────────────────── + + def tap(self, x: int, y: int) -> bool: + """Tap at device coordinates (x, y).""" + return self._run(["shell", "input", "tap", str(int(x)), str(int(y))]) + + def swipe( + self, + x1: int, + y1: int, + x2: int, + y2: int, + duration_ms: int = 800, + ) -> bool: + """ + Swipe from (x1, y1) to (x2, y2). + + Args: + x1, y1: Start position in device pixels. + x2, y2: End position in device pixels. + duration_ms: Swipe duration in milliseconds (default 800 ms). + """ + return self._run([ + "shell", "input", "swipe", + str(int(x1)), str(int(y1)), + str(int(x2)), str(int(y2)), + str(int(duration_ms)), + ]) + + def wheel_zoom_out_approx( + self, + center_x: int, + center_y: int, + times: int = 5, + interval_sec: float = 0.1, + arm_px: int = 200, + duration_ms: int = 150, + ) -> None: + """ + Approximate a mouse-wheel \"zoom out\" using short upward swipes from the center. + + Used when the game is driven only via ADB (no Win32 mouse wheel). + """ + cx, cy = int(center_x), int(center_y) + for _ in range(max(1, int(times))): + y1 = cy + arm_px + y2 = cy - arm_px + self.swipe(cx, y1, cx, y2, duration_ms) + time.sleep(max(0.0, float(interval_sec))) + + +# ── Module-level singleton ───────────────────────────────────────────────────── +# Set once by main.py after emulator mode is determined; None in PC mode. + +_instance: Optional[AdbInput] = None + + +def get_adb_input() -> Optional[AdbInput]: + """Return the active AdbInput instance, or None when in PC (win32) mode.""" + return _instance + + +def set_adb_input(inst: Optional[AdbInput]) -> None: + """Set (or clear) the global AdbInput instance. Called once by main.py.""" + global _instance + prev = _instance + if prev is not None and prev is not inst: + proc = getattr(prev, "_getevent_proc", None) + if proc is not None: + try: + proc.kill() + except Exception: + pass + prev._getevent_proc = None + prev._getevent_log_thread = None + _instance = inst diff --git a/kha_lastz_auto/alliance_attack_detector.py b/kha_lastz_auto/alliance_attack_detector.py new file mode 100644 index 0000000..9b4df43 --- /dev/null +++ b/kha_lastz_auto/alliance_attack_detector.py @@ -0,0 +1,54 @@ +import time +from vision import Vision + + +class AllianceAttackDetector: + """Detect alliance-being-attacked state by matching the AllianceBeingAttackedWarning icon. + + - Attack starts: icon found in any single frame. + - Attack ends: icon absent for `clear_sec` consecutive seconds. + Zalo notification is configured in function YAML (event_type: send_zalo) and triggered by config. + """ + + def __init__(self, warning_template_path: str, + threshold: float = 0.6, clear_sec: float = 10.0): + self._vision = Vision(warning_template_path) + self._threshold = threshold + self._clear_sec = clear_sec + self._attacked = False + self._clear_since = None + + def reset(self): + """Reset state so the next update re-evaluates from scratch (e.g. after Is Running toggled back ON).""" + self._attacked = False + self._clear_since = None + + def update(self, screenshot, log): + """Call once per captured frame. + + Returns: + "started" — alliance attack just began this frame + "ended" — alliance attack just ended this frame + None — no state change + """ + icon = self._vision.exists(screenshot, threshold=self._threshold) + now = time.time() + + if not self._attacked: + if icon: + self._attacked = True + self._clear_since = None + log.info("[Alert] Alliance is being attacked!") + return "started" + else: + if icon: + self._clear_since = None + else: + if self._clear_since is None: + self._clear_since = now + elif now - self._clear_since >= self._clear_sec: + self._attacked = False + self._clear_since = None + log.info("[Alert] Alliance attack has ended.") + return "ended" + return None diff --git a/kha_lastz_auto/attack_detector.py b/kha_lastz_auto/attack_detector.py new file mode 100644 index 0000000..18b4298 --- /dev/null +++ b/kha_lastz_auto/attack_detector.py @@ -0,0 +1,64 @@ +import time +from vision import Vision + +try: + import zalo_clicker +except ImportError: + zalo_clicker = None + + +class AttackDetector: + """Detect being-attacked state by matching the BeingAttackedWarning icon. + + - Attack starts: icon found in any single frame. + - Attack ends: icon absent for `clear_sec` consecutive seconds. + Zalo message is configured in function YAML (event_type: send_zalo) and triggered by config. + """ + + def __init__(self, warning_template_path: str, + threshold: float = 0.7, clear_sec: float = 10.0): + self._vision = Vision(warning_template_path) + self._threshold = threshold + self._clear_sec = clear_sec + self._attacked = False + self._clear_since = None + + def reset(self): + """Reset state so the next update re-evaluates from scratch (e.g. after Is Running toggled back ON).""" + self._attacked = False + self._clear_since = None + + def update(self, screenshot, log): + """Call once per captured frame. + + Returns: + "started" — attack just began this frame + "ended" — attack just ended this frame + None — no state change + """ + icon = self._vision.exists(screenshot, threshold=self._threshold) + now = time.time() + + if not self._attacked: + if icon: + self._attacked = True + self._clear_since = None + log.info("[Alert] House is being attacked!") + if zalo_clicker: + try: + zalo_clicker.run_zalo_click(logger=log) + except Exception as e: + log.warning("[ZaloClicker] %s", e) + return "started" + else: + if icon: + self._clear_since = None # still attacked → reset countdown + else: + if self._clear_since is None: + self._clear_since = now # start countdown + elif now - self._clear_since >= self._clear_sec: + self._attacked = False + self._clear_since = None + log.info("[Alert] Attack has ended.") + return "ended" + return None diff --git a/kha_lastz_auto/bot_engine.py b/kha_lastz_auto/bot_engine.py new file mode 100644 index 0000000..4a3f79b --- /dev/null +++ b/kha_lastz_auto/bot_engine.py @@ -0,0 +1,1473 @@ +""" +Engine chay Function (YAML): load functions, thuc thi tung step. +Step types: match_click, match_storm_click, match_multi_click, match_count, sleep, send_zalo, click_position, wait_until_match, key_press, set_level, type_text, click_unless_visible, drag, close_ui, base_zoomout, world_zoomout. +Each step returns true/false. Next step is blocked (function aborted) if previous returned false, unless run_always: true is set. +send_zalo: message (required), receiver_name (optional; ten hien thi trong danh sach chat, fallback DEFAULT_CLICK_AFTER_OPEN), repeat_interval_sec (optional; when set and trigger_active_cb provided, repeats while trigger active). +""" +import os +import re +import time +import logging +import random +import yaml +import pyautogui +import cv2 as cv +from vision import Vision, get_global_scale + +from pynput.mouse import Button, Controller +from fast_clicker import FastClicker +from window_click_guard import WindowClickGuard +from ocr_utils import ( + read_level_from_roi as _read_level_from_roi, + read_raw_text_from_roi as _read_raw_text_from_roi, + read_region_relative, + _parse_level, +) + +log = logging.getLogger("kha_lastz") +_mouse_ctrl = Controller() + +try: + import zalo_web_clicker as _zalo_web_clicker +except ImportError: + _zalo_web_clicker = None + + +def _save_debug_image(screenshot, raw_center, click_center, needle_w, needle_h, event_type, template_path, + truck_name=None): + """Save a debug PNG with green rect (match area) and red circle (click/move target). + For YellowTruckSmall: also saves a tight crop of the truck. All files include a timestamp. + truck_name: optional string overlaid on the YellowTruckSmall crop.""" + import datetime + ts = datetime.datetime.now().strftime("%H%M%S_%f")[:-3] # HHMMSSmmm + tname = os.path.splitext(os.path.basename(template_path))[0] + os.makedirs("debug_ocr", exist_ok=True) + + # Full screenshot with rect + dot + dbg = screenshot.copy() + rx = raw_center[0] - needle_w // 2 + ry = raw_center[1] - needle_h // 2 + cv.rectangle(dbg, (rx, ry), (rx + needle_w, ry + needle_h), (0, 255, 0), 2) + cv.circle(dbg, (click_center[0], click_center[1]), 12, (0, 0, 255), -1) + out_path = os.path.join("debug_ocr", "debug_{}_{}_{}.png".format(event_type, tname, ts)) + cv.imwrite(out_path, dbg) + log.info("[Runner] debug_click saved → {}".format(os.path.abspath(out_path))) + + # YellowTruckSmall: crop tight around the truck + overlay name + if "YellowTruckSmall" in tname: + pad_x = max(30, needle_w * 6) # wide left padding to show player name + pad_y = max(20, needle_h) + h, w = screenshot.shape[:2] + x1 = max(0, rx - pad_x) + y1 = max(0, ry - pad_y) + x2 = min(w, rx + needle_w + 20) + y2 = min(h, ry + needle_h + pad_y) + crop = screenshot[y1:y2, x1:x2].copy() + lrx, lry = rx - x1, ry - y1 + cv.rectangle(crop, (lrx, lry), (lrx + needle_w, lry + needle_h), (0, 255, 0), 2) + cx, cy = click_center[0] - x1, click_center[1] - y1 + if 0 <= cx < crop.shape[1] and 0 <= cy < crop.shape[0]: + cv.circle(crop, (cx, cy), 10, (0, 0, 255), -1) + if truck_name: + label = truck_name + font_scale = max(0.5, needle_h / 30.0) + thickness = max(1, int(font_scale * 1.5)) + (tw, th), _ = cv.getTextSize(label, cv.FONT_HERSHEY_SIMPLEX, font_scale, thickness) + tx = max(0, lrx - tw - 4) + ty = lry + needle_h // 2 + th // 2 + cv.rectangle(crop, (tx - 2, ty - th - 2), (tx + tw + 2, ty + 2), (0, 0, 0), -1) + cv.putText(crop, label, (tx, ty), cv.FONT_HERSHEY_SIMPLEX, + font_scale, (0, 255, 255), thickness, cv.LINE_AA) + safe_name = (truck_name or "").replace("/", "_").replace("\\", "_").replace(" ", "_")[:20] + crop_fname = "YellowTruck_{}_{}.png".format(safe_name, ts) if safe_name else "YellowTruck_crop_{}.png".format(ts) + crop_path = os.path.join("debug_ocr", crop_fname) + cv.imwrite(crop_path, crop) + log.info("[Runner] debug_click saved (truck crop) → {}".format(os.path.abspath(crop_path))) + return crop_path # caller can store this to rename/overlay once player name is known + return None + + +def _retitle_truck_crop(old_path, player_name): + """Reload existing truck crop, overlay player name, save under new name including the name.""" + if not old_path or not os.path.isfile(old_path): + return old_path + img = cv.imread(old_path) + if img is None: + return old_path + # Overlay name text at top-left + label = player_name + font_scale, thickness = 0.6, 2 + (tw, th), _ = cv.getTextSize(label, cv.FONT_HERSHEY_SIMPLEX, font_scale, thickness) + cv.rectangle(img, (4, 4), (tw + 10, th + 12), (0, 0, 0), -1) + cv.putText(img, label, (7, th + 7), cv.FONT_HERSHEY_SIMPLEX, + font_scale, (0, 255, 255), thickness, cv.LINE_AA) + # Build new filename: insert safe name before the timestamp suffix + base = os.path.basename(old_path) + root, ext = os.path.splitext(base) + safe_name = player_name.replace("/", "_").replace("\\", "_").replace(" ", "_")[:25] + # root is like "YellowTruck_crop_HHMMSS_mmm" — replace "crop" with the safe name + new_root = root.replace("_crop_", "_{}_".format(safe_name), 1) + if new_root == root: # fallback: just append + new_root = "{}_{}".format(root, safe_name) + new_path = os.path.join(os.path.dirname(old_path), new_root + ext) + cv.imwrite(new_path, img) + try: + os.remove(old_path) + except OSError: + pass + log.info("[Runner] truck crop retitled → {}".format(os.path.abspath(new_path))) + return new_path + + +def load_functions(functions_dir="functions"): + """Load tat ca file YAML trong functions_dir. Tra ve dict: ten_function -> { description, steps }.""" + result = {} + if not os.path.isdir(functions_dir): + return result + for fname in os.listdir(functions_dir): + if not fname.endswith(".yaml") and not fname.endswith(".yml"): + continue + name = os.path.splitext(fname)[0] + path = os.path.join(functions_dir, fname) + try: + with open(path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + steps = data.get("steps", []) + name_to_index = { + s["name"]: i + for i, s in enumerate(steps) + if isinstance(s, dict) and s.get("name") + } + result[name] = { + "description": data.get("description", ""), + "steps": steps, + "name_to_index": name_to_index, + "run_count": data.get("run_count", 1), + } + # log.debug("[bot_engine] Loaded function: {}".format(name)) + except Exception as e: + log.error("[bot_engine] Failed to load {}: {}".format(path, e)) + return result + + +def _crop_region_relative(screenshot, cx, cy, needle_w, needle_h, + x=0.0, y=0.0, w=1.0, h=1.0): + """Crop a sub-region relative to a template match center. + x/y/w/h are ratios of the needle (template) size — same coordinate space as read_region_relative. + Returns the cropped BGR image, or None if out of bounds.""" + tl_x = cx - needle_w // 2 + tl_y = cy - needle_h // 2 + rx = max(0, tl_x + int(x * needle_w)) + ry = max(0, tl_y + int(y * needle_h)) + rw = max(1, int(w * needle_w)) + rh = max(1, int(h * needle_h)) + img_h, img_w = screenshot.shape[:2] + rw = min(rw, img_w - rx) + rh = min(rh, img_h - ry) + if rw <= 0 or rh <= 0: + return None + crop = screenshot[ry: ry + rh, rx: rx + rw] + return crop if crop.size > 0 else None + + +def _iter_templates(step, single_key, list_key): + """Yield all template paths from a step that supports both a single key and a list key.""" + tpl_list = step.get(list_key) + if tpl_list and isinstance(tpl_list, list): + for t in tpl_list: + if t: + yield t + else: + tpl = step.get(single_key) + if tpl: + yield tpl + + +def collect_templates(functions_dict): + """Lay danh sach duong dan template tu tat ca steps de tao vision cache.""" + templates = set() + for fn in functions_dict.values(): + for step in fn["steps"]: + if step.get("event_type") in ( + "match_click", "match_multi_click", "match_count", "match_move", + "match_storm_click", + ) and step.get("template"): + templates.add(step["template"]) + if step.get("event_type") == "match_click" and step.get("template_array"): + ta = step.get("template_array") + if isinstance(ta, list): + for t in ta: + path = t.get("template") if isinstance(t, dict) else t + if path: + templates.add(path) + if step.get("event_type") == "match_click" and step.get("refresh_template"): + templates.add(step["refresh_template"]) + if step.get("event_type") == "wait_until_match" and step.get("template"): + templates.add(step["template"]) + if step.get("event_type") == "click_unless_visible": + if step.get("visible_template"): + templates.add(step["visible_template"]) + if step.get("click_template"): + templates.add(step["click_template"]) + if step.get("event_type") == "set_level": + if step.get("plus_template"): + templates.add(step["plus_template"]) + if step.get("minus_template"): + templates.add(step["minus_template"]) + if step.get("level_anchor_template"): + templates.add(step["level_anchor_template"]) + if step.get("event_type") in ("base_zoomout", "close_ui", "world_zoomout"): + if step.get("template"): + templates.add(step["template"]) + if step.get("world_button"): + templates.add(step["world_button"]) + if step.get("back_button"): + templates.add(step["back_button"]) + if step.get("event_type") == "ocr_log": + tpl = step.get("anchor_template") or step.get("template") + if tpl: + templates.add(tpl) + if step.get("event_type") == "find_truck": + from events import event_find_truck as _ev_find_truck + for tpl in _ev_find_truck.collect_templates(step): + templates.add(tpl) + if step.get("event_type") == "arena_filter": + from events import event_arena_filter as _ev_arena_filter + for tpl in _ev_arena_filter.collect_templates(step): + templates.add(tpl) + return list(templates) + + +def build_vision_cache(template_paths): + """Tao dict template_path -> Vision(...).""" + cache = {} + for path in template_paths: + try: + cache[path] = Vision(path) + except Exception as e: + log.info("[bot_engine] Failed to load template {}: {}".format(path, e)) + return cache + + +class FunctionRunner: + """Chay 1 function: giu state step hien tai, xu ly tung step theo type.""" + + def __init__(self, vision_cache, fn_settings=None, bot_paused=None): + self.vision_cache = vision_cache + self.fn_settings = fn_settings if fn_settings is not None else {} + self.bot_paused = bot_paused # ref to {"paused": bool}; when True, send_zalo repeat counter resets on resume + self.functions = {} + self.state = "idle" # idle | running + self.function_name = None + self.start_reason = None # last start source (scheduler reason string); set in start(), cleared in stop() + self.steps = [] + self.step_index = 0 + self.step_start_time = None + self.step_click_count = 0 + self.wincap = None + self.last_step_result = True # True = previous step matched/succeeded + self._step_retry_counts = {} # {step_index: retry_count} for on_fail_goto + self._step_visit_counts = {} # {step_index: visit_count} for max_tries / on_max_tries_reach_goto + self._step_visit_start_times = {} # {step_index: step_start_time} to detect new visits + self._step_dedup_positions = {} # {step_index: [(x, y)]} positions already clicked (cross-visit dedup) + self._tried_positions = [] # [(x, y)] positions already clicked in current cycle + self._step_name_map = {} # {name: step_index} built from steps with a "name" field + self._rtr_cache = {} # require_text_in_region cache: frozenset(points) → filtered points + self._rtr_page_logged = False # True after printing truck list for current refresh cycle + self._fast_clicker = FastClicker() + self._window_click_guard = WindowClickGuard() + self._storm_clicker_active = False # True while match_storm_click is running + self._storm_start_t: float = 0.0 + self._storm_clicker_kwargs: dict = {} # last start() args, used to restart after offset change + self._storm_offset_restart_t: float | None = None # time when offset_changed was first detected + self._storm_corner_restart_t: float | None = None # delay before isolated corner click + FastClicker restart + self._match_click_storm_kwargs: dict = {} # match_click click_storm: args for FastClicker restart + self._step_pos_cache = None + self._debug_click_saved = False + self._step_last_click_t = None + self._run_count_remaining = 0 + + def _fn_setting(self, key, fallback=None): + """Return fn_settings[current_function][key], or fallback if not set.""" + return self.fn_settings.get(self.function_name or "", {}).get(key, fallback) + + def load(self, functions_dict): + self.functions = functions_dict + + def start(self, function_name, wincap, trigger_event=None, trigger_active_cb=None, start_reason=None): + # Intentionally no hot-reload on start(). + # Functions and fn_settings are loaded at app startup and updated in-memory by UI actions. + + if function_name not in self.functions: + log.info("[Runner] Function not found: {}".format(function_name)) + return False + self.function_name = function_name + self.steps = self.functions[function_name]["steps"] + self._step_name_map = self.functions[function_name].get("name_to_index", {}) + self.step_index = 0 + self.step_start_time = time.time() + self.step_click_count = 0 + self.wincap = wincap + self.state = "running" + self.last_step_result = True + self.trigger_event = trigger_event + self.trigger_active_cb = trigger_active_cb + self._step_retry_counts = {} + self._step_visit_counts = {} + self._step_visit_start_times = {} + self._step_dedup_positions = {} + self._tried_positions = [] + # Reset storm state so a re-run of the same (or a new) function starts clean. + self._fast_clicker.stop() + self._window_click_guard.stop() + self._storm_clicker_active = False + self._storm_start_t = 0.0 + self._storm_clicker_kwargs = {} + self._storm_offset_restart_t = None + self._storm_corner_restart_t = None + self._match_click_storm_kwargs = {} + self._last_zero_refresh_t = 0 + self._last_truck_crop_path = None + self._ocr_prev_vals = {} # {step_index: last_read_value} for wait_for_change_region + self._last_click_pos = None # position of the most recent match_click (template-space) + self._last_ocr_click_pos = None # position that was current when ocr_log last ran + self._tpl_array_idx = 0 # current template index for template_array steps + for attr in ("_set_level_debug_saved", "_set_level_warned"): + if hasattr(self, attr): + delattr(self, attr) + self._world_zoomout_start = None + # Determine total run_count: fn_settings override > YAML default + yaml_run_count = self.functions[function_name].get("run_count", 1) + setting_run_count = self.fn_settings.get(function_name, {}).get("run_count") + try: + run_count = int(setting_run_count) if setting_run_count is not None else int(yaml_run_count) + except (ValueError, TypeError): + run_count = 1 + self._run_count_remaining = max(1, run_count) - 1 + self.start_reason = start_reason + _reason = start_reason if start_reason else "unspecified" + log.info("[Runner] Started function: {} (run_count={}, reason={})".format( + function_name, max(1, run_count), _reason)) + return True + + def _get_vision(self, template): + """Get Vision object from cache, or load on-the-fly if missing/new.""" + if not template: + return None + v = self.vision_cache.get(template) + if v: + return v + + # On-the-fly load attempt + if os.path.isfile(template): + try: + log.info("[Runner] Template {} not in cache, loading on-the-fly...".format(template)) + v = Vision(template) + self.vision_cache[template] = v + return v + except Exception as e: + log.error("[Runner] Failed to load template {} on-the-fly: {}".format(template, e)) + return None + + def stop(self): + self._fast_clicker.stop() + self._window_click_guard.stop() + self._storm_clicker_active = False + try: + import ctypes + ctypes.windll.user32.BlockInput(False) + except Exception: + pass + self.state = "idle" + self.function_name = None + self.start_reason = None + + def abort_current_function(self, reason: str = "user stop") -> bool: + """Stop the active YAML function only. Does not change global pause (bot_paused). + + Returns True if a function was running and was aborted. + """ + if self.state != "running": + return False + name = self.function_name + log.info("[Runner] Aborted function: {} ({})".format(name, reason)) + self.stop() + return True + + def _runner_still_active(self) -> bool: + """True only while this function run is active. + + ``UserMouseAbortThread`` can call ``stop()`` while the game loop is still inside + ``update()``; long step bodies (e.g. ``close_ui``) must poll this and bail out. + """ + return self.state == "running" + + def update(self, screenshot, wincap): + """Tra ve 'running' | 'done' | 'idle'. Neu running thi xu ly step hien tai.""" + if self.state != "running" or screenshot is None or self.step_index >= len(self.steps): + if self.state == "running" and self.step_index >= len(self.steps): + self._fast_clicker.stop() + self._window_click_guard.stop() + self._storm_clicker_active = False + try: + import ctypes + ctypes.windll.user32.BlockInput(False) + except Exception: + pass + suffix = "" if self.last_step_result else " (aborted)" + log.info("[Runner] Finished function: {}{}".format(self.function_name, suffix)) + if self._run_count_remaining > 0: + self._run_count_remaining -= 1 + _sr = self.start_reason if self.start_reason else "unspecified" + log.info("[Runner] Repeating function: {} ({} run(s) remaining, reason={})".format( + self.function_name, self._run_count_remaining, _sr)) + self.step_index = 0 + self.step_start_time = time.time() + self.step_click_count = 0 + self.last_step_result = True + self._step_retry_counts = {} + self._step_visit_counts = {} + self._step_visit_start_times = {} + self._step_dedup_positions = {} + self._tried_positions = [] + self._rtr_cache = {} + self._rtr_page_logged = False + self._step_pos_cache = None + self._debug_click_saved = False + self._step_last_click_t = None + self._tpl_array_idx = 0 + self._tpl_array_start_t = None + self._tpl_array_last_tpl = None + self._ocr_prev_vals = {} + self._last_click_pos = None + self._last_ocr_click_pos = None + self._world_zoomout_start = None + return "running" + self.state = "idle" + return "done" + return "idle" if self.state == "idle" else "running" + + self.wincap = wincap + step = self.steps[self.step_index] + step_type = step.get("event_type", "") + + # Step result gate: skip this step if previous returned False, + # UNLESS this step has run_always: true (always executes regardless). + # Skip one step at a time so run_always steps later in the list still get reached. + run_always = step.get("run_always", False) + if not run_always and not self.last_step_result: + log.info("[Runner] [skip] {}".format(self._step_label(step))) + self._advance_step(False) + return "running" + + now = time.time() + if self.step_start_time is None: + self.step_start_time = now + + if step_type == "match_click": + from events import event_match_click as _ev_match_click + return _ev_match_click.run(step, screenshot, wincap, self) + + if step_type == "match_move": + from events import event_match_move as _ev_match_move + return _ev_match_move.run(step, screenshot, wincap, self) + + if step_type == "match_multi_click": + # Find ALL visible instances of template and click each one, then advance. + # If none found within timeout_sec, advance anyway. + template = step.get("template") + threshold = step.get("threshold", 0.75) + timeout_sec = step.get("timeout_sec") or 10 + click_interval_sec = step.get("click_interval_sec", 0.15) + + vision = self._get_vision(template) + if not vision: + self._advance_step(True) + return "running" + points = vision.find(screenshot, threshold=threshold, debug_mode=None) + if points: + import adb_input as _adb_mod + _adb = _adb_mod.get_adb_input() + for pt in points: + if _adb is not None: + _adb.tap(pt[0], pt[1]) + else: + sx, sy = wincap.get_screen_position((pt[0], pt[1])) + try: + if not self._safe_move(sx, sy, wincap, "match_multi_click"): + continue + time.sleep(0.05) + except Exception: + pass + if hasattr(wincap, "focus_window"): + wincap.focus_window() + _mouse_ctrl.press(Button.left) + time.sleep(0.1) + _mouse_ctrl.release(Button.left) + if click_interval_sec > 0: + time.sleep(click_interval_sec) + log.info("[Runner] {} → true (clicked {} match(es))".format(self._step_label(step), len(points))) + self._advance_step(True) + return "running" + if now - self.step_start_time >= timeout_sec: + log.info("[Runner] {} → false (not found in {}s)".format(self._step_label(step), timeout_sec)) + self._advance_step(False) + return "running" + + if step_type == "sleep": + from events import event_sleep as _ev_sleep + return _ev_sleep.run(step, screenshot, wincap, self) + + if step_type == "send_zalo": + from events import event_send_zalo as _ev_send_zalo + return _ev_send_zalo.run(step, screenshot, wincap, self) + + if step_type == "click_position": + from events import event_click_position as _ev_click_pos + return _ev_click_pos.run(step, screenshot, wincap, self) + + if step_type == "wait_until_match": + template = step.get("template") + threshold = step.get("threshold", 0.75) + timeout_sec = step.get("timeout_sec") or 30 + vision = self._get_vision(template) + if not vision: + self._advance_step(True) + return "running" + points = vision.find(screenshot, threshold=threshold, debug_mode=None) + if points: + log.info("[Runner] {} → true".format(self._step_label(step))) + self._advance_step(True) + return "running" + if now - self.step_start_time >= timeout_sec: + log.info("[Runner] {} → false (not found in {}s)".format(self._step_label(step), timeout_sec)) + self._advance_step(False) + return "running" + + if step_type == "set_level": + from events import event_set_level as _ev_set_level + return _ev_set_level.run(step, screenshot, wincap, self) + + if step_type == "click_unless_visible": + # If visible_template is found on screen -> skip (already on right screen). + # If NOT found -> click click_template to navigate there, then advance. + visible_template = step.get("visible_template") + click_template = step.get("click_template") + threshold = step.get("threshold", 0.75) + timeout_sec = step.get("timeout_sec", 3) + v_check = self.vision_cache.get(visible_template) if visible_template else None + if v_check and v_check.find(screenshot, threshold=threshold, debug_mode=None): + log.info("[Runner] {} → true (visible, skip nav)".format(self._step_label(step))) + self._advance_step(True) + return "running" + if now - self.step_start_time >= timeout_sec: + v_nav = self.vision_cache.get(click_template) if click_template else None + if v_nav: + pts = v_nav.find(screenshot, threshold=threshold, debug_mode=None) + if pts: + sx, sy = wincap.get_screen_position((pts[0][0], pts[0][1])) + self._safe_click(sx, sy, wincap, "click_unless_visible") + log.info("[Runner] {} → true (not visible, clicked nav)".format(self._step_label(step))) + else: + log.info("[Runner] {} → true (not visible, nav absent too)".format(self._step_label(step))) + self._advance_step(True) + return "running" + + if step_type == "key_press": + key = step.get("key", "") + if key: + pyautogui.press(key) + log.info("[Runner] {} → true".format(self._step_label(step))) + self._advance_step(True) + return "running" + + if step_type == "type_text": + text = str(step.get("text", "")) + # Resolve ${ENV_VAR} placeholders — fn_settings takes priority over os.environ + def _resolve_var(m): + env_key = m.group(1) + # Map known env vars to fn_settings keys + _env_to_setting = {"PIN_PASSWORD": "password"} + setting_key = _env_to_setting.get(env_key) + if setting_key: + from_settings = self._fn_setting(setting_key) + if from_settings is not None and str(from_settings).strip(): + return str(from_settings) + return os.environ.get(env_key, "") + text = re.sub(r"\$\{([^}]+)\}", _resolve_var, text) + interval = step.get("interval_sec", 0.1) + if text: + pyautogui.write(text, interval=interval) + log.info("[Runner] {} → true ({} chars)".format(self._step_label(step), len(text))) + else: + log.info("[Runner] {} → true (empty — check .env / ${{}} var name)".format(self._step_label(step))) + self._advance_step(True) + return "running" + + if step_type == "match_count": + # Returns true if template appears >= count times within timeout_sec, false otherwise. + # Does NOT click anything. + template = step.get("template") + count = step.get("count", 1) + threshold = step.get("threshold", 0.75) + timeout_sec = step.get("timeout_sec") or 10 + debug_save = step.get("debug_save", False) + vision = self.vision_cache.get(template) + if not vision: + self._advance_step(True) + return "running" + match_color = step.get("match_color", False) + debug_log = step.get("debug_log", False) + points = vision.find(screenshot, threshold=threshold, debug_mode=None, + is_color=match_color, debug_log=debug_log, multi=True) + found = len(points) if points else 0 + if found >= count: + log.info("[Runner] {} → true (found {}/{})".format(self._step_label(step), found, count)) + self._advance_step(True) + return "running" + if now - self.step_start_time >= timeout_sec: + log.info("[Runner] {} → false (found {}/{}, timeout {}s)".format( + self._step_label(step), found, count, timeout_sec)) + if debug_save: + try: + os.makedirs("debug", exist_ok=True) + ts_str = time.strftime("%Y%m%d_%H%M%S") + tpl_name = os.path.splitext(os.path.basename(template))[0] + # Save screenshot + shot_path = os.path.join("debug", "match_count_{}_{}_screenshot.png".format(tpl_name, ts_str)) + cv.imwrite(shot_path, screenshot) + # Save template for comparison + tpl_path = os.path.join("debug", "match_count_{}_{}_template.png".format(tpl_name, ts_str)) + cv.imwrite(tpl_path, vision.needle_img) + log.info("[Runner] match_count debug saved: screenshot={}x{} template={}x{} -> {}".format( + screenshot.shape[1], screenshot.shape[0], + vision.needle_w, vision.needle_h, + shot_path)) + except Exception as e: + log.info("[Runner] match_count debug save failed: {}".format(e)) + on_fail_goto = step.get("on_fail_goto") + max_retries = step.get("max_retries") # None = unlimited retries + if on_fail_goto is not None: + if max_retries is None: + # No limit — retry indefinitely + log.info("[Runner] {} → retry (goto step {})".format( + self._step_label(step), on_fail_goto)) + self._goto_step(self._resolve_goto(on_fail_goto)) + return "running" + else: + max_retries = int(max_retries) + cur_step_idx = self.step_index + retry_count = self._step_retry_counts.get(cur_step_idx, 0) + 1 + if retry_count <= max_retries: + self._step_retry_counts[cur_step_idx] = retry_count + log.info("[Runner] {} → retry {}/{} (goto step {})".format( + self._step_label(step), retry_count, max_retries, on_fail_goto)) + self._goto_step(self._resolve_goto(on_fail_goto)) + return "running" + log.info("[Runner] {} → max_retries ({}) reached → abort".format( + self._step_label(step), max_retries)) + self._advance_step(False) + return "running" + + if step_type == "close_ui": + # Vong lap: neu chua thay HQ & World thi click 1 cai (click_x, click_y), chup lai, kiem tra lai; thoat khi da thay. + template = step.get("template") + world_button = step.get("world_button") + threshold = step.get("threshold", 0.75) + # back_button uses its own threshold (default higher) to avoid false positives + back_button_threshold = float(step.get("back_button_threshold", 0.80)) + debug_log = step.get("debug_log", False) + debug_save = step.get("debug_save", False) + match_color = step.get("match_color", True) + color_tol = step.get("color_match_tolerance", 80) + click_x = float(step.get("click_x", 0.03)) + click_y = float(step.get("click_y", 0.08)) + max_tries = int(step.get("max_tries", 10)) + back_button = step.get("back_button") + vision = self.vision_cache.get(template) if template else None + vision_world = self.vision_cache.get(world_button) if world_button else None + vision_back = self.vision_cache.get(back_button) if back_button else None + dbg_mode = "info" if debug_log else None + _roi_offset = (0, 0) + _roi_bounds = None + _roi_cx = step.get("roi_center_x", 0.93) + _roi_cy = step.get("roi_center_y", 0.96) + if _roi_cx is not None and _roi_cy is not None and vision: + _sh, _sw = screenshot.shape[:2] + _scale = get_global_scale() + _nw_px = max(1, int(vision.needle_w * _scale)) + _nh_px = max(1, int(vision.needle_h * _scale)) + _padding = float(step.get("roi_padding", 2.0)) + _cx_px = int(_roi_cx * _sw) + _cy_px = int(_roi_cy * _sh) + _half_w = int(_nw_px * _padding) + _half_h = int(_nh_px * _padding) + _rx = max(0, _cx_px - _half_w) + _ry = max(0, _cy_px - _half_h) + _rx2 = min(_sw, _cx_px + _half_w) + _ry2 = min(_sh, _cy_px + _half_h) + _roi_offset = (_rx, _ry) + _roi_bounds = (_rx, _ry, _rx2, _ry2) + + def _close_ui_search_img(img): + if _roi_bounds is None: + return img + rx, ry, rx2, ry2 = _roi_bounds + return img[ry:ry2, rx:rx2] + + def _close_ui_shift_points(pts): + if not pts or _roi_offset == (0, 0): + return pts + ox, oy = _roi_offset + return [((p[0] + ox, p[1] + oy) + tuple(p[2:]) if len(p) > 2 else (p[0] + ox, p[1] + oy)) for p in pts] + + scr = screenshot + for _try in range(max_tries): + # Stop immediately if the bot was paused/cancelled mid-loop + if self.bot_paused and self.bot_paused.get("paused", False): + if debug_log: + log.info("[Runner] close_ui → aborted (bot paused)") + return "running" + if not self._runner_still_active(): + return "idle" + + _search = _close_ui_search_img(scr) + _phq = vision.find(_search, threshold=threshold, debug_mode=dbg_mode, debug_log=debug_log, + is_color=bool(match_color), color_tolerance=color_tol) if vision else [] + _phq = _close_ui_shift_points(_phq if _phq else []) + _pw = vision_world.find(_search, threshold=threshold, debug_mode=dbg_mode, debug_log=debug_log, + is_color=bool(match_color), color_tolerance=color_tol) if vision_world else [] + _pw = _close_ui_shift_points(_pw if _pw else []) + if debug_save: + try: + import datetime + _ts = datetime.datetime.now().strftime("%H%M%S_%f")[:-3] + _dbg_dir = "debug_close_ui" + os.makedirs(_dbg_dir, exist_ok=True) + _dbg_img = _search.copy() + # Draw ROI bounds info as text + _label = "try={} hq={} world={} color={}".format(_try, len(_phq), len(_pw), match_color) + cv.putText(_dbg_img, _label, (5, 20), cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 1) + # Draw match rectangles if found + for _pt in _phq: + _mx, _my = (_pt[0] - _roi_offset[0], _pt[1] - _roi_offset[1]) + if vision: + cv.rectangle(_dbg_img, (_mx - vision.needle_w//2, _my - vision.needle_h//2), + (_mx + vision.needle_w//2, _my + vision.needle_h//2), (0, 255, 0), 2) + for _pt in _pw: + _mx, _my = (_pt[0] - _roi_offset[0], _pt[1] - _roi_offset[1]) + if vision_world: + cv.rectangle(_dbg_img, (_mx - vision_world.needle_w//2, _my - vision_world.needle_h//2), + (_mx + vision_world.needle_w//2, _my + vision_world.needle_h//2), (0, 0, 255), 2) + _fname = os.path.join(_dbg_dir, "close_ui_try{}_{}.png".format(_try, _ts)) + cv.imwrite(_fname, _dbg_img) + log.info("[Runner] close_ui debug_save → {}".format(os.path.abspath(_fname))) + except Exception as _de: + log.warning("[Runner] close_ui debug_save failed: {}".format(_de)) + if _phq or _pw: + if debug_log: + log.info("[Runner] close_ui → true (thay HQ/World sau {} lan click)".format(_try)) + break + if _try < max_tries - 1: + if hasattr(wincap, "focus_window"): + wincap.focus_window(force=True) + time.sleep(0.05) + # Priority: click BackButton if visible; fallback to default (click_x, click_y) + _clicked_back = False + if vision_back: + _pb = vision_back.find(scr, threshold=back_button_threshold, + debug_mode=dbg_mode, debug_log=debug_log) + if _pb: + _bx, _by = _pb[0][0], _pb[0][1] + _bsx, _bsy = wincap.get_screen_position((_bx, _by)) + self._safe_click(_bsx, _bsy, wincap, "close_ui BackButton") + if debug_log: + log.info("[Runner] close_ui → clicked BackButton ({},{})".format(_bx, _by)) + _clicked_back = True + if not _clicked_back: + _px = int(wincap.w * click_x) + _py = int(wincap.h * click_y) + _sx, _sy = wincap.get_screen_position((_px, _py)) + self._safe_click(_sx, _sy, wincap, "close_ui default") + time.sleep(1) + _fresh = None + try: + from screenshot_provider import get_active_capture_service + + _svc = get_active_capture_service() + if _svc is not None: + _fresh = _svc.capture_frame() + except Exception: + pass + if _fresh is not None: + scr = _fresh + time.sleep(0.3) + if not self._runner_still_active(): + return "idle" + log.info("[Runner] close_ui → true") + self._advance_step(True) + return "running" + + if step_type == "base_zoomout": + from events import event_base_zoomout as _ev_base_zo + return _ev_base_zo.run(step, screenshot, wincap, self) + + if step_type == "world_zoomout": + from events import event_world_zoomout as _ev_world_zo + return _ev_world_zo.run(step, screenshot, wincap, self) + + if step_type == "drag": + # start_x, start_y = diem bat dau (ti le 0-1). direction_x/y = huong, magnitude nhan voi do dai keo (vd 5 -> keo dai gap 5). + # drag_distance_ratio = ti le so voi canh nho cua so (mac dinh 0.15). drag_duration_sec = thoi gian moi lan keo. + dir_x = float(step.get("direction_x", step.get("x", 0))) + dir_y = float(step.get("direction_y", step.get("y", 0))) + count = max(1, int(step.get("count", 3))) + start_x_ratio = float(step.get("start_x", 0.5)) + start_y_ratio = float(step.get("start_y", 0.5)) + if hasattr(wincap, "focus_window"): + wincap.focus_window(force=True) + time.sleep(0.05) + # Diem bat dau (client px) -> screen px + px_start = int(wincap.w * start_x_ratio) + py_start = int(wincap.h * start_y_ratio) + sx, sy = wincap.get_screen_position((px_start, py_start)) + length = (dir_x * dir_x + dir_y * dir_y) ** 0.5 + if length < 1e-6: + dx, dy = 1.0, 0.0 + length = 1.0 + else: + dx, dy = dir_x / length, dir_y / length + # Do dai keo = base * max(1, magnitude(direction)). direction (5,0) -> keo dai gap 5. + ratio = float(step.get("drag_distance_ratio", 0.15)) + base_px = ratio * min(wincap.w, wincap.h) + drag_distance_px = base_px * max(1.0, length) + ex = int(sx + dx * drag_distance_px) + ey = int(sy + dy * drag_distance_px) + offset_x = ex - sx + offset_y = ey - sy + # Thoi gian cho ca qua trinh keo (mac dinh cham rai ~0.8s) + duration = step.get("drag_duration_sec", 0.8) + num_steps = max(15, int(step.get("drag_steps", 30))) + + log.info("[Runner] drag: win={}x{}, start screen=({},{}), end=({},{}), offset=({},{}), drag_px={:.0f}, steps={}, duration={}s".format( + wincap.w, wincap.h, sx, sy, ex, ey, offset_x, offset_y, drag_distance_px, num_steps, duration)) + + import adb_input as _adb_mod + _adb = _adb_mod.get_adb_input() + if _adb is not None: + # ADB mode: swipe with client pixel coords (start + end computed in client space) + ex_client = px_start + int(dx * drag_distance_px) + ey_client = py_start + int(dy * drag_distance_px) + duration_ms = int(duration * 1000) + log.info("[Runner] drag (ADB): start=(%d,%d) end=(%d,%d) duration=%dms x%d", + px_start, py_start, ex_client, ey_client, duration_ms, count) + for i in range(count): + _adb.swipe(px_start, py_start, ex_client, ey_client, duration_ms) + if i < count - 1: + time.sleep(0.12) + else: + # Win32 mode: Windows API SetCursorPos + mouse_event + # block_input=True uses BlockInput() to ignore user input during drag + import ctypes + _MOUSEEVENTF_LEFTDOWN = 0x0002 + _MOUSEEVENTF_LEFTUP = 0x0004 + block_input = step.get("block_input", True) + + def _do_drag(sx_, sy_, off_x, off_y): + try: + import ctypes + u32 = ctypes.windll.user32 + ex_ = sx_ + off_x + ey_ = sy_ + off_y + step_duration = duration / num_steps + log.info("[Runner] drag step 1: SetCursorPos start ({}, {})".format(sx_, sy_)) + u32.SetCursorPos(int(sx_), int(sy_)) + time.sleep(0.06) + log.info("[Runner] drag step 2: mouse_event LEFTDOWN at ({}, {})".format(sx_, sy_)) + u32.mouse_event(_MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) + time.sleep(0.05) + for s in range(1, num_steps + 1): + t = s / num_steps + px = int(sx_ + off_x * t) + py = int(sy_ + off_y * t) + u32.SetCursorPos(px, py) + time.sleep(step_duration) + if s == 1 or s == num_steps or s == num_steps // 2: + log.info("[Runner] drag step 3: move s={}/{} -> ({}, {})".format(s, num_steps, px, py)) + time.sleep(0.04) + log.info("[Runner] drag step 4: mouse_event LEFTUP at ({}, {})".format(int(ex_), int(ey_))) + u32.mouse_event(_MOUSEEVENTF_LEFTUP, 0, 0, 0, 0) + except Exception as e: + log.warning("[Runner] drag exception: {}".format(e)) + + try: + if block_input: + try: + if ctypes.windll.user32.BlockInput(True): + log.info("[Runner] drag: BlockInput(True) — blocking user input during drag") + else: + log.warning("[Runner] drag: BlockInput(True) returned False (e.g. UAC) — continuing without block") + except Exception as e: + log.warning("[Runner] drag: BlockInput(True) failed: {} — continuing without block".format(e)) + for i in range(count): + _do_drag(sx, sy, offset_x, offset_y) + if i < count - 1: + time.sleep(0.12) + finally: + if block_input: + try: + ctypes.windll.user32.BlockInput(False) + log.info("[Runner] drag: BlockInput(False) — user input restored") + except Exception as e: + log.warning("[Runner] drag: BlockInput(False) failed: {}".format(e)) + log.info("[Runner] {} → true (drag dir=({},{}) x{})".format( + self._step_label(step), step.get("direction_x", step.get("x")), step.get("direction_y", step.get("y")), count)) + self._advance_step(True) + return "running" + + if step_type == "ocr_log": + # OCR text from a region, then log the result. Always advances — never blocks the flow. + # + # Mode A — anchor template + pixel offset (single region): + # anchor_template: template to find for position + # anchor_offset: [ox, oy, w, h] px from anchor center to OCR region + # + # Mode A2 — anchor template + ratio regions (multiple regions): + # anchor_template: template to find for position + # ocr_regions: list of {name, x, y, w, h, digits_only, pattern} + # x/y/w/h are ratios of template size (0.0–1.0) + # + # Mode B — absolute ROI (no template needed, most robust): + # roi_ratios: [x, y, w, h] as fractions of screen size (0.0–1.0) + # + # debug_save: true → save ROI crop (and screenshot if anchor not found) + # exit_on_true: true → when anchor IS found, end function (same semantic as match_click exit_on_true) + anchor_template = step.get("anchor_template") or step.get("template") + roi_ratios = step.get("roi_ratios") # [x, y, w, h] 0-1 fractions + ocr_regions = step.get("ocr_regions") # list of ratio-based region dicts + threshold = step.get("threshold", 0.75) + anchor_offset = step.get("anchor_offset") # [ox, oy, w, h] px from anchor center + char_whitelist = step.get("char_whitelist") + label = step.get("label", "ocr_log") + debug_save = step.get("debug_save", False) + timeout_sec = step.get("timeout_sec", 5) + exit_on_true = step.get("exit_on_true", False) + _debug_key = "_ocr_log_debug_{}".format(label.replace(" ", "_")) + + # ── Mode B: roi_ratios — run immediately, no template needed ────── + if roi_ratios and len(roi_ratios) == 4: + h_img, w_img = screenshot.shape[:2] + rx, ry, rw, rh = roi_ratios + x = max(0, int(rx * w_img)) + y = max(0, int(ry * h_img)) + w = max(1, int(rw * w_img)) + h = max(1, int(rh * h_img)) + w = min(w, w_img - x) + h = min(h, h_img - y) + roi = screenshot[y:y + h, x:x + w] + debug_path = None + if debug_save and not getattr(self, _debug_key, False): + debug_path = "debug_ocr_{}.png".format(label.replace(" ", "_")) + setattr(self, _debug_key, True) + # anchor_center=(0,0) + offset=(x,y,w,h) → crops exactly [x:x+w, y:y+h] + text = _read_raw_text_from_roi( + screenshot, (0, 0), [x, y, w, h], + char_whitelist=char_whitelist, debug_save_path=debug_path) + if text: + log.info("[Runner] {} [{}]: {}".format(self._step_label(step), label, text)) + else: + log.info("[Runner] {} [{}]: (no text read)".format(self._step_label(step), label)) + if debug_path: + log.info("[Runner] ocr_log: ROI saved to {}".format(debug_path)) + _on_success_goto = step.get("on_success_goto") + if text and str(text).strip() and _on_success_goto is not None: + log.info("[Runner] {} → on_success_goto {}".format(self._step_label(step), _on_success_goto)) + self._goto_step(self._resolve_goto(_on_success_goto)) + elif exit_on_true and text and str(text).strip(): + self._advance_step(True, step=step) + else: + self._advance_step(True) + return "running" + + # ── Mode B2: ocr_regions only (no anchor) — x,y,w,h = tỉ lệ màn hình (0.0–1.0) ─ + if ocr_regions and not anchor_template: + h_img, w_img = screenshot.shape[:2] + any_text = False + for region in ocr_regions: + rname = region.get("name", "ocr") + rx = float(region.get("x", 0.0)) + ry = float(region.get("y", 0.0)) + rw = float(region.get("w", 1.0)) + rh = float(region.get("h", 0.2)) + x = max(0, int(rx * w_img)) + y = max(0, int(ry * h_img)) + w = max(1, int(rw * w_img)) + h = max(1, int(rh * h_img)) + w = min(w, w_img - x) + h = min(h, h_img - y) + debug_path = None + if debug_save: + debug_path = "debug_ocr_{}_{}.png".format(label.replace(" ", "_"), rname) + text = _read_raw_text_from_roi( + screenshot, (0, 0), [x, y, w, h], + char_whitelist=char_whitelist, debug_save_path=debug_path) + if text: + s = str(text).strip() + any_text = any_text or (bool(s) and any(c.isdigit() for c in s)) + log.info("[Runner] {} [{}]: {}".format(self._step_label(step), rname, text)) + else: + log.info("[Runner] {} [{}]: (no text read)".format(self._step_label(step), rname)) + _on_success_goto = step.get("on_success_goto") + if any_text and _on_success_goto is not None: + log.info("[Runner] {} → on_success_goto {}".format(self._step_label(step), _on_success_goto)) + self._goto_step(self._resolve_goto(_on_success_goto)) + elif exit_on_true and any_text: + self._advance_step(True, step=step) + else: + self._advance_step(True) + return "running" + + # ── Mode A / A2: anchor template ────────────────────────────────── + if not anchor_template or (not anchor_offset and not ocr_regions): + log.info("[Runner] {} → skip (set roi_ratios, ocr_regions only, or anchor_template + anchor_offset/ocr_regions)".format(self._step_label(step))) + self._advance_step(True) + return "running" + + vision = self.vision_cache.get(anchor_template) + if not vision: + log.info("[Runner] {} → skip (anchor_template not loaded)".format(self._step_label(step))) + self._advance_step(True) + return "running" + + points = vision.find(screenshot, threshold=threshold, debug_mode=None) + if points: + pt = points[0] + cx, cy, mw, mh = (pt[0], pt[1], pt[2], pt[3]) if len(pt) >= 4 else (pt[0], pt[1], vision.needle_w, vision.needle_h) + tpl_name = os.path.splitext(os.path.basename(anchor_template))[0] + + # ── require_new_click: gate OCR on a fresh truck click ──────────── + # If set, only proceed when _last_click_pos differs from the position + # recorded on the previous OCR run — i.e. a NEW truck was clicked. + # No extra OCR call needed; the click coordinates are the signal. + if step.get("require_new_click", False): + _cur_pos = self._last_click_pos + _prev_pos = self._last_ocr_click_pos + if _cur_pos is not None and _cur_pos == _prev_pos: + # Same click as before — wait until a new truck is clicked + if now - self.step_start_time < timeout_sec: + return "running" + # Timeout: no new click detected → treat as assertion failure + log.info("[Runner] {} [require_new_click]: timeout, no new click detected".format( + self._step_label(step))) + on_fail_goto = step.get("on_fail_goto") + max_retries = int(step.get("max_retries", 0)) + if on_fail_goto is not None and max_retries > 0: + cur_step_idx = self.step_index + retry_count = self._step_retry_counts.get(cur_step_idx, 0) + 1 + if retry_count <= max_retries: + self._step_retry_counts[cur_step_idx] = retry_count + log.info("[Runner] {} [require_new_click]: retry {}/{} (goto step {})".format( + self._step_label(step), retry_count, max_retries, on_fail_goto)) + self._goto_step(self._resolve_goto(on_fail_goto)) + return "running" + log.info("[Runner] {} [require_new_click]: max_retries ({}) reached → abort".format( + self._step_label(step), max_retries)) + self._advance_step(False) + return "running" + # New click position detected — record it and proceed + if _cur_pos != _prev_pos: + self._last_ocr_click_pos = _cur_pos + if _prev_pos is not None: + log.info("[Runner] {} [require_new_click]: new click {} → {}, proceeding".format( + self._step_label(step), _prev_pos, _cur_pos)) + # ───────────────────────────────────────────────────────────────── + + if ocr_regions: + # Mode A2: multiple ratio-based regions relative to template size + for region in ocr_regions: + rname = region.get("name", "ocr") + dbg_lbl = "{}_{}".format(tpl_name, rname) if debug_save else None + text = read_region_relative( + screenshot, cx, cy, + mw, mh, + x = region.get("x", 0.0), + y = region.get("y", 0.0), + w = region.get("w", 1.0), + h = region.get("h", 1.0), + digits_only = region.get("digits_only", False), + pattern = region.get("pattern"), + debug_label = dbg_lbl, + ) + log.info("[Runner] {} [{}]: {}".format(self._step_label(step), rname, text or "(no text)")) + if dbg_lbl: + log.info("[Runner] ocr_log: debug crops → debug_ocr/{}_*.png".format(dbg_lbl)) + + # ── Retitle last truck crop with player name ─────────── + if rname == "player_name" and text: + _crop = getattr(self, '_last_truck_crop_path', None) + if _crop: + self._last_truck_crop_path = _retitle_truck_crop(_crop, text) + + # ── Assertions ──────────────────────────────────────── + assert_eq = region.get("assert_equals") # exact string match + assert_in = region.get("assert_in") # match any value in list + assert_max = region.get("assert_max") # numeric: fail if value >= max + assert_min = region.get("assert_min") # numeric: fail if value < min + # fn_settings overrides for TruckPlunder (and any function with these keys) + if rname == "server": + _srv_ov = self._fn_setting("servers") + if _srv_ov is not None and str(_srv_ov).strip(): + _srv_str = str(_srv_ov).strip() + if _srv_str == "*": + assert_in = None # wildcard: accept any server + else: + assert_in = [s.strip() for s in _srv_str.split(",") if s.strip()] + if rname == "power": + _mp_ov = self._fn_setting("max_power") + if _mp_ov is not None: + try: + assert_max = int(_mp_ov) + except (ValueError, TypeError): + pass + + _assert_fail_reason = None + + if assert_in is not None: + allowed = [str(v) for v in (assert_in if isinstance(assert_in, list) else [assert_in])] + if text not in allowed: + _assert_fail_reason = "assert_in FAIL ({!r} not in {})".format(text, allowed) + elif assert_eq is not None and text != str(assert_eq): + _assert_fail_reason = "assert_equals FAIL ({!r} != {!r})".format(text, str(assert_eq)) + + if _assert_fail_reason is None and (assert_max is not None or assert_min is not None): + try: + num = int(text.replace(",", "").replace(".", "")) + if assert_max is not None and num >= int(assert_max): + _assert_fail_reason = "assert_max FAIL ({} >= {})".format(num, assert_max) + elif assert_min is not None and num < int(assert_min): + _assert_fail_reason = "assert_min FAIL ({} < {})".format(num, assert_min) + except (ValueError, AttributeError): + _assert_fail_reason = "assert numeric FAIL (cannot parse {!r})".format(text) + + if _assert_fail_reason: + on_fail_goto = step.get("on_fail_goto") + max_retries = step.get("max_retries") # None = unlimited retries + cur_step_idx = self.step_index + + if on_fail_goto is not None: + if max_retries is None: + # No limit — retry indefinitely + log.info("[Runner] {} [{}]: {} → retry (goto step {})".format( + self._step_label(step), rname, _assert_fail_reason, on_fail_goto)) + self._goto_step(self._resolve_goto(on_fail_goto)) + return "running" + else: + max_retries = int(max_retries) + retry_count = self._step_retry_counts.get(cur_step_idx, 0) + 1 + if retry_count <= max_retries: + self._step_retry_counts[cur_step_idx] = retry_count + log.info("[Runner] {} [{}]: {} → retry {}/{} (goto step {})".format( + self._step_label(step), rname, _assert_fail_reason, + retry_count, max_retries, on_fail_goto)) + self._goto_step(self._resolve_goto(on_fail_goto)) + return "running" + log.info("[Runner] {} [{}]: {} → max_retries ({}) reached → abort".format( + self._step_label(step), rname, _assert_fail_reason, max_retries)) + else: + log.info("[Runner] {} [{}]: {} → abort".format( + self._step_label(step), rname, _assert_fail_reason)) + + self._advance_step(False) + return "running" + else: + # Mode A: single pixel-offset region + debug_path = None + if debug_save and not getattr(self, _debug_key, False): + debug_path = "debug_ocr_{}.png".format(label.replace(" ", "_")) + setattr(self, _debug_key, True) + text = _read_raw_text_from_roi( + screenshot, (cx, cy), anchor_offset, + char_whitelist=char_whitelist, + debug_save_path=debug_path, + ) + if text: + log.info("[Runner] {} [{}]: {}".format(self._step_label(step), label, text)) + else: + log.info("[Runner] {} [{}]: (no text read)".format(self._step_label(step), label)) + if debug_path: + log.info("[Runner] ocr_log: ROI saved to {}".format(debug_path)) + + if exit_on_true: + self._advance_step(True, step=step) + else: + self._advance_step(True) + return "running" + + if now - self.step_start_time >= timeout_sec: + if exit_on_true: + # anchor NOT found → condition not met → continue with next steps + log.info("[Runner] {} → continue (exit_on_true but anchor not found in {}s)".format(self._step_label(step), timeout_sec)) + else: + log.info("[Runner] {} → anchor not found in {}s".format(self._step_label(step), timeout_sec)) + if debug_save and not getattr(self, _debug_key + "_shot", False): + setattr(self, _debug_key + "_shot", True) + try: + shot_path = "debug_ocr_{}_screen.png".format(label.replace(" ", "_")) + cv.imwrite(shot_path, screenshot) + log.info("[Runner] ocr_log: anchor not found — screen saved to {}".format(shot_path)) + except Exception as e: + log.info("[Runner] ocr_log: failed to save debug screen: {}".format(e)) + + if not exit_on_true: + # Anchor not found = cannot verify conditions → treat as assertion failure + on_fail_goto = step.get("on_fail_goto") + max_retries = int(step.get("max_retries", 0)) + cur_step_idx = self.step_index + if on_fail_goto is not None and max_retries > 0: + retry_count = self._step_retry_counts.get(cur_step_idx, 0) + 1 + if retry_count <= max_retries: + self._step_retry_counts[cur_step_idx] = retry_count + log.info("[Runner] {} → anchor not found → retry {}/{} (goto step {})".format( + self._step_label(step), retry_count, max_retries, on_fail_goto)) + self._goto_step(self._resolve_goto(on_fail_goto)) + return "running" + else: + log.info("[Runner] {} → anchor not found → max_retries ({}) reached → abort".format( + self._step_label(step), max_retries)) + self._advance_step(False) + else: + self._advance_step(True) + return "running" + + if step_type == "find_truck": + # Logic lives in events/event_find_truck.py — see that file for full documentation. + from events import event_find_truck as _ev_find_truck + return _ev_find_truck.run(step, screenshot, wincap, self) + + # ── arena_filter ───────────────────────────────────────────────────────── + # Logic lives in events/event_arena_filter.py — see that file for full documentation. + if step_type == "arena_filter": + from events import event_arena_filter as _ev_arena_filter + return _ev_arena_filter.run(step, screenshot, wincap, self) + + # ── match_storm_click ──────────────────────────────────────────────────── + # Dedicated storm-click step: find template → FastClick storm. + # YAML keys: + # template, threshold (default 0.75) + # timeout_sec — wait up to N seconds for template (default 999) + # storm_sec — max storm duration once found (default 60) + # max_clicks — stop after this many clicks (default 0 = unlimited) + # offset_x/y — ±random click offset as fraction of window size (default 0) + # e.g. 0.03 = ±3% of window width/height in pixels + # position_refresh_sec — re-detect template every N seconds and reposition (default 0 = off) + # template gone on refresh → storm stops immediately + # offset_change_time — seconds between offset re-randomizations (default 1.0) + # all clicks within the window land on the same pixel + # offset_change_pause_sec — idle after offset rollover before restart (default 0.2) + # storm_click_interval_sec — sleep after each storm click (default 0.1); or storm_click_max_rate + # close_ui_check — every 1s, if template not visible click once at close_ui + # position to dismiss any accidentally-opened UI (default true) + # close_ui_click_x/y — fractional position to click when dismissing UI (default 0.03, 0.08) + # close_ui_back_button — optional template path for a BackButton; clicked instead of + # close_ui_click_x/y when visible + # corner — {offset_x, offset_y, every} to keep window focused (optional) + if step_type == "match_storm_click": + from events import event_match_storm_click as _ev_msc + return _ev_msc.run(step, screenshot, wincap, self) + + # unknown type -> skip (true so next step still runs) + self._advance_step(True) + return "running" + + def _safe_click(self, sx: int, sy: int, wincap, label: str = "") -> bool: + """Click at screen coords (sx, sy) only if inside game window bounds. + + In LDPlayer (ADB) mode, converts screen → client coords and uses ADB tap. + Returns True when the click fires, False when skipped (out of bounds). + """ + import adb_input as _adb_mod + _adb = _adb_mod.get_adb_input() + if _adb is not None: + # ADB mode: convert screen → client pixel coords and tap + client_x = sx - wincap.offset_x + client_y = sy - wincap.offset_y + if 0 <= client_x < wincap.w and 0 <= client_y < wincap.h: + return _adb.tap(int(client_x), int(client_y)) + log.warning( + "[Runner] safe_click (ADB) skipped (%d,%d) → client (%d,%d) outside %dx%d%s", + sx, sy, int(client_x), int(client_y), wincap.w, wincap.h, + " [{}]".format(label) if label else "", + ) + return False + + _l, _t = wincap.get_screen_position((0, 0)) + _r = _l + wincap.w + _b = _t + wincap.h + if not (_l <= sx < _r and _t <= sy < _b): + log.warning( + "[Runner] safe_click skipped (%d,%d) outside game window (%d,%d)→(%d,%d)%s", + sx, sy, _l, _t, _r, _b, + " [{}]".format(label) if label else "", + ) + return False + pyautogui.click(sx, sy) + return True + + def _safe_move(self, sx: int, sy: int, wincap, label: str = "") -> bool: + """Move mouse to (sx, sy) only if inside game window bounds. + + Returns True when the move is applied, False when skipped. + Caller is responsible for the subsequent press/release. + """ + _l, _t = wincap.get_screen_position((0, 0)) + _r = _l + wincap.w + _b = _t + wincap.h + if not (_l <= sx < _r and _t <= sy < _b): + log.warning( + "[Runner] safe_move skipped (%d,%d) outside game window (%d,%d)→(%d,%d)%s", + sx, sy, _l, _t, _r, _b, + " [{}]".format(label) if label else "", + ) + return False + try: + import user_mouse_abort as _uma + _uma.suppress_trip_for_sec(0.18) + except Exception: + pass + _mouse_ctrl.position = (sx, sy) + return True + + def _step_label(self, step): + """Return a short description of a step for use in log messages. + + If the step has a ``name`` field, it is prepended as ``[name]`` so named + steps are immediately identifiable in the log. + """ + stype = step.get("event_type", "?") + step_name = step.get("name") + tpl = step.get("template") or step.get("click_template") or "" + tpl_name = os.path.splitext(os.path.basename(tpl))[0] if tpl else "" + if stype == "sleep": + base = "sleep {}s".format(step.get("duration_sec", 0)) + elif stype == "click_position": + if step.get("position_setting_key"): + base = "click_position ({} from setting)".format(step.get("position_setting_key")) + else: + x = step.get("x", step.get("offset_x", 0)) + y = step.get("y", step.get("offset_y", 0)) + base = "click_position (x={}, y={})".format(x, y) + elif stype == "type_text": + base = "type_text" + elif stype == "key_press": + base = "key_press {}".format(step.get("key", "")) + elif stype == "set_level": + base = "set_level Lv.{}".format(step.get("target_level", "?")) + elif stype == "drag": + dx = step.get("direction_x", step.get("x", 0)) + dy = step.get("direction_y", step.get("y", 0)) + c = step.get("count", 3) + start = step.get("start_x"), step.get("start_y") + if start[0] is not None or start[1] is not None: + base = "drag dir=({},{}) start=({},{}) x{}".format(dx, dy, start[0] or 0.5, start[1] or 0.5, c) + else: + base = "drag dir=({},{}) x{}".format(dx, dy, c) + elif tpl_name: + base = "{} {}".format(stype, tpl_name) + else: + base = stype + if step_name: + return "[{}] {}".format(step_name, base) + return base + + def _advance_step(self, result=True, step=None): + self.step_index += 1 + self.step_start_time = time.time() + self.step_click_count = 0 + self.last_step_result = result + self._step_last_click_t = None + self._debug_click_saved = False + self._step_pos_cache = None + self._tpl_array_idx = 0 + self._tpl_array_start_t = None + self._tpl_array_last_tpl = None + if step is not None: + if step.get("exit_always"): + self.step_index = len(self.steps) + log.info("[Runner] {} → exit_always → end function".format(self._step_label(step))) + elif step.get("exit_on_true") and result: + self.step_index = len(self.steps) + log.info("[Runner] {} → exit_on_true (match) → end function".format(self._step_label(step))) + + def _resolve_goto(self, value) -> int: + """Resolve a goto value to a step index. + + Accepts either an integer index or a string step name defined via the ``name`` + field on any event. Returns the resolved integer index, or the current + step_index as a no-op fallback when the name is not found. + """ + if isinstance(value, int): + return value + try: + return int(value) + except (ValueError, TypeError): + idx = self._step_name_map.get(str(value)) + if idx is None: + log.warning("[Runner] goto name '{}' not found in current function, staying on current step".format(value)) + return self.step_index + return idx + + def _goto_step(self, index): + """Jump to a specific step index (used for retry loops).""" + self.step_index = index + self.step_start_time = time.time() + self.step_click_count = 0 + self.last_step_result = True + self._step_last_click_t = None + self._debug_click_saved = False + self._step_pos_cache = None + # Reset template_array state so the target step starts from template 0. + self._tpl_array_idx = 0 + self._tpl_array_start_t = None + self._tpl_array_last_tpl = None + + def _fail_step(self, step, reason: str = "") -> None: + """Handle a step failure: jump to on_fail_goto (with optional max_retries), or advance normally. + + This centralises the on_fail_goto logic so every event type can use it + without duplicating the retry-counter bookkeeping. + """ + on_fail_goto = step.get("on_fail_goto") + if on_fail_goto is None: + self._advance_step(False, step=step) + return + + max_retries = step.get("max_retries") # None = unlimited + label = self._step_label(step) + target = self._resolve_goto(on_fail_goto) + + if max_retries is None: + log.info("[Runner] {} {} → goto step {} (unlimited retries)".format( + label, reason, target)) + self._goto_step(target) + return + + max_retries = int(max_retries) + cur_idx = self.step_index + retry_count = self._step_retry_counts.get(cur_idx, 0) + 1 + if retry_count <= max_retries: + self._step_retry_counts[cur_idx] = retry_count + log.info("[Runner] {} {} → goto step {} (retry {}/{})".format( + label, reason, target, retry_count, max_retries)) + self._goto_step(target) + else: + log.info("[Runner] {} {} → max_retries ({}) reached → advance".format( + label, reason, max_retries)) + self._advance_step(False, step=step) + + +def load_config(config_path="config.yaml"): + """Load config.yaml. Tra ve dict co key_bindings, schedules.""" + if not os.path.isfile(config_path): + return {"key_bindings": {}, "schedules": []} + with open(config_path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) or {"key_bindings": {}, "schedules": []} diff --git a/kha_lastz_auto/buttons_template/AllianceBeingAttackedWarning.png b/kha_lastz_auto/buttons_template/AllianceBeingAttackedWarning.png new file mode 100644 index 0000000..3ffcf59 Binary files /dev/null and b/kha_lastz_auto/buttons_template/AllianceBeingAttackedWarning.png differ diff --git a/kha_lastz_auto/buttons_template/AllianceBeingAttackedWarning1.png b/kha_lastz_auto/buttons_template/AllianceBeingAttackedWarning1.png new file mode 100644 index 0000000..55aa8c2 Binary files /dev/null and b/kha_lastz_auto/buttons_template/AllianceBeingAttackedWarning1.png differ diff --git a/kha_lastz_auto/buttons_template/AllianceButton.png b/kha_lastz_auto/buttons_template/AllianceButton.png new file mode 100644 index 0000000..3ab98ef Binary files /dev/null and b/kha_lastz_auto/buttons_template/AllianceButton.png differ diff --git a/kha_lastz_auto/buttons_template/AllianceClaimAll.png b/kha_lastz_auto/buttons_template/AllianceClaimAll.png new file mode 100644 index 0000000..be87d45 Binary files /dev/null and b/kha_lastz_auto/buttons_template/AllianceClaimAll.png differ diff --git a/kha_lastz_auto/buttons_template/AllianceGifts.png b/kha_lastz_auto/buttons_template/AllianceGifts.png new file mode 100644 index 0000000..e513cde Binary files /dev/null and b/kha_lastz_auto/buttons_template/AllianceGifts.png differ diff --git a/kha_lastz_auto/buttons_template/AllianceIconButton.png b/kha_lastz_auto/buttons_template/AllianceIconButton.png new file mode 100644 index 0000000..0f3e8ea Binary files /dev/null and b/kha_lastz_auto/buttons_template/AllianceIconButton.png differ diff --git a/kha_lastz_auto/buttons_template/AllianceTechDonate.png b/kha_lastz_auto/buttons_template/AllianceTechDonate.png new file mode 100644 index 0000000..089b4c1 Binary files /dev/null and b/kha_lastz_auto/buttons_template/AllianceTechDonate.png differ diff --git a/kha_lastz_auto/buttons_template/AllianceTechDonate1.png b/kha_lastz_auto/buttons_template/AllianceTechDonate1.png new file mode 100644 index 0000000..e644d06 Binary files /dev/null and b/kha_lastz_auto/buttons_template/AllianceTechDonate1.png differ diff --git a/kha_lastz_auto/buttons_template/AllianceTechLike.png b/kha_lastz_auto/buttons_template/AllianceTechLike.png new file mode 100644 index 0000000..1792050 Binary files /dev/null and b/kha_lastz_auto/buttons_template/AllianceTechLike.png differ diff --git a/kha_lastz_auto/buttons_template/AllianceTechLike1.png b/kha_lastz_auto/buttons_template/AllianceTechLike1.png new file mode 100644 index 0000000..862b093 Binary files /dev/null and b/kha_lastz_auto/buttons_template/AllianceTechLike1.png differ diff --git a/kha_lastz_auto/buttons_template/AllianceTechs.png b/kha_lastz_auto/buttons_template/AllianceTechs.png new file mode 100644 index 0000000..e5462b2 Binary files /dev/null and b/kha_lastz_auto/buttons_template/AllianceTechs.png differ diff --git a/kha_lastz_auto/buttons_template/AlloyNormal.png b/kha_lastz_auto/buttons_template/AlloyNormal.png new file mode 100644 index 0000000..c1e8962 Binary files /dev/null and b/kha_lastz_auto/buttons_template/AlloyNormal.png differ diff --git a/kha_lastz_auto/buttons_template/Arena.png b/kha_lastz_auto/buttons_template/Arena.png new file mode 100644 index 0000000..bec5c3f Binary files /dev/null and b/kha_lastz_auto/buttons_template/Arena.png differ diff --git a/kha_lastz_auto/buttons_template/ArenaChallenge.png b/kha_lastz_auto/buttons_template/ArenaChallenge.png new file mode 100644 index 0000000..cbeaacb Binary files /dev/null and b/kha_lastz_auto/buttons_template/ArenaChallenge.png differ diff --git a/kha_lastz_auto/buttons_template/ArenaCloseButton.png b/kha_lastz_auto/buttons_template/ArenaCloseButton.png new file mode 100644 index 0000000..381fd06 Binary files /dev/null and b/kha_lastz_auto/buttons_template/ArenaCloseButton.png differ diff --git a/kha_lastz_auto/buttons_template/ArenaCombat.png b/kha_lastz_auto/buttons_template/ArenaCombat.png new file mode 100644 index 0000000..e5e3c70 Binary files /dev/null and b/kha_lastz_auto/buttons_template/ArenaCombat.png differ diff --git a/kha_lastz_auto/buttons_template/ArenaFight.png b/kha_lastz_auto/buttons_template/ArenaFight.png new file mode 100644 index 0000000..3ffb252 Binary files /dev/null and b/kha_lastz_auto/buttons_template/ArenaFight.png differ diff --git a/kha_lastz_auto/buttons_template/ArenaIcon.png b/kha_lastz_auto/buttons_template/ArenaIcon.png new file mode 100644 index 0000000..2138cb5 Binary files /dev/null and b/kha_lastz_auto/buttons_template/ArenaIcon.png differ diff --git a/kha_lastz_auto/buttons_template/ArenaIcon2.png b/kha_lastz_auto/buttons_template/ArenaIcon2.png new file mode 100644 index 0000000..e765b08 Binary files /dev/null and b/kha_lastz_auto/buttons_template/ArenaIcon2.png differ diff --git a/kha_lastz_auto/buttons_template/ArenaQuickCompleteBattle.png b/kha_lastz_auto/buttons_template/ArenaQuickCompleteBattle.png new file mode 100644 index 0000000..cf13c6e Binary files /dev/null and b/kha_lastz_auto/buttons_template/ArenaQuickCompleteBattle.png differ diff --git a/kha_lastz_auto/buttons_template/ArenaQuickDeploy.png b/kha_lastz_auto/buttons_template/ArenaQuickDeploy.png new file mode 100644 index 0000000..b5c14fe Binary files /dev/null and b/kha_lastz_auto/buttons_template/ArenaQuickDeploy.png differ diff --git a/kha_lastz_auto/buttons_template/AssaultCollect.png b/kha_lastz_auto/buttons_template/AssaultCollect.png new file mode 100644 index 0000000..1d6fcb8 Binary files /dev/null and b/kha_lastz_auto/buttons_template/AssaultCollect.png differ diff --git a/kha_lastz_auto/buttons_template/BackButton.png b/kha_lastz_auto/buttons_template/BackButton.png new file mode 100644 index 0000000..92f50b8 Binary files /dev/null and b/kha_lastz_auto/buttons_template/BackButton.png differ diff --git a/kha_lastz_auto/buttons_template/BeingAttackedWarning.png b/kha_lastz_auto/buttons_template/BeingAttackedWarning.png new file mode 100644 index 0000000..4c0a6a2 Binary files /dev/null and b/kha_lastz_auto/buttons_template/BeingAttackedWarning.png differ diff --git a/kha_lastz_auto/buttons_template/BeingAttackedWarning1.png b/kha_lastz_auto/buttons_template/BeingAttackedWarning1.png new file mode 100644 index 0000000..b770eb3 Binary files /dev/null and b/kha_lastz_auto/buttons_template/BeingAttackedWarning1.png differ diff --git a/kha_lastz_auto/buttons_template/BoomerButton.png b/kha_lastz_auto/buttons_template/BoomerButton.png new file mode 100644 index 0000000..2f1d5bb Binary files /dev/null and b/kha_lastz_auto/buttons_template/BoomerButton.png differ diff --git a/kha_lastz_auto/buttons_template/BountyMissionClaimButton.png b/kha_lastz_auto/buttons_template/BountyMissionClaimButton.png new file mode 100644 index 0000000..4d67ee4 Binary files /dev/null and b/kha_lastz_auto/buttons_template/BountyMissionClaimButton.png differ diff --git a/kha_lastz_auto/buttons_template/BountyMissionDispatchButton.png b/kha_lastz_auto/buttons_template/BountyMissionDispatchButton.png new file mode 100644 index 0000000..598949a Binary files /dev/null and b/kha_lastz_auto/buttons_template/BountyMissionDispatchButton.png differ diff --git a/kha_lastz_auto/buttons_template/BountyMissionGoButton.png b/kha_lastz_auto/buttons_template/BountyMissionGoButton.png new file mode 100644 index 0000000..a279f77 Binary files /dev/null and b/kha_lastz_auto/buttons_template/BountyMissionGoButton.png differ diff --git a/kha_lastz_auto/buttons_template/BountyMissionIcon.png b/kha_lastz_auto/buttons_template/BountyMissionIcon.png new file mode 100644 index 0000000..ac3a51b Binary files /dev/null and b/kha_lastz_auto/buttons_template/BountyMissionIcon.png differ diff --git a/kha_lastz_auto/buttons_template/BountyMissionQuickDeploy.png b/kha_lastz_auto/buttons_template/BountyMissionQuickDeploy.png new file mode 100644 index 0000000..946d23f Binary files /dev/null and b/kha_lastz_auto/buttons_template/BountyMissionQuickDeploy.png differ diff --git a/kha_lastz_auto/buttons_template/BuffIcon.png b/kha_lastz_auto/buttons_template/BuffIcon.png new file mode 100644 index 0000000..2046de2 Binary files /dev/null and b/kha_lastz_auto/buttons_template/BuffIcon.png differ diff --git a/kha_lastz_auto/buttons_template/ClaimAll.png b/kha_lastz_auto/buttons_template/ClaimAll.png new file mode 100644 index 0000000..4f2304b Binary files /dev/null and b/kha_lastz_auto/buttons_template/ClaimAll.png differ diff --git a/kha_lastz_auto/buttons_template/ClaimButton.png b/kha_lastz_auto/buttons_template/ClaimButton.png new file mode 100644 index 0000000..7dc9cd2 Binary files /dev/null and b/kha_lastz_auto/buttons_template/ClaimButton.png differ diff --git a/kha_lastz_auto/buttons_template/ClaimVivianDailyReward.png b/kha_lastz_auto/buttons_template/ClaimVivianDailyReward.png new file mode 100644 index 0000000..9401679 Binary files /dev/null and b/kha_lastz_auto/buttons_template/ClaimVivianDailyReward.png differ diff --git a/kha_lastz_auto/buttons_template/CollectButton.png b/kha_lastz_auto/buttons_template/CollectButton.png new file mode 100644 index 0000000..66df8a5 Binary files /dev/null and b/kha_lastz_auto/buttons_template/CollectButton.png differ diff --git a/kha_lastz_auto/buttons_template/CollectMiaBlueprint.png b/kha_lastz_auto/buttons_template/CollectMiaBlueprint.png new file mode 100644 index 0000000..d185bcd Binary files /dev/null and b/kha_lastz_auto/buttons_template/CollectMiaBlueprint.png differ diff --git a/kha_lastz_auto/buttons_template/ConfirmButton.png b/kha_lastz_auto/buttons_template/ConfirmButton.png new file mode 100644 index 0000000..ef17d85 Binary files /dev/null and b/kha_lastz_auto/buttons_template/ConfirmButton.png differ diff --git a/kha_lastz_auto/buttons_template/CureSolider1.png b/kha_lastz_auto/buttons_template/CureSolider1.png new file mode 100644 index 0000000..ba81ccf Binary files /dev/null and b/kha_lastz_auto/buttons_template/CureSolider1.png differ diff --git a/kha_lastz_auto/buttons_template/CureSolider2.png b/kha_lastz_auto/buttons_template/CureSolider2.png new file mode 100644 index 0000000..3a291fd Binary files /dev/null and b/kha_lastz_auto/buttons_template/CureSolider2.png differ diff --git a/kha_lastz_auto/buttons_template/CureSolider3.png b/kha_lastz_auto/buttons_template/CureSolider3.png new file mode 100644 index 0000000..87bc7eb Binary files /dev/null and b/kha_lastz_auto/buttons_template/CureSolider3.png differ diff --git a/kha_lastz_auto/buttons_template/CureSolider4.png b/kha_lastz_auto/buttons_template/CureSolider4.png new file mode 100644 index 0000000..faac8e7 Binary files /dev/null and b/kha_lastz_auto/buttons_template/CureSolider4.png differ diff --git a/kha_lastz_auto/buttons_template/CureSoliderHealButton.png b/kha_lastz_auto/buttons_template/CureSoliderHealButton.png new file mode 100644 index 0000000..bd0130b Binary files /dev/null and b/kha_lastz_auto/buttons_template/CureSoliderHealButton.png differ diff --git a/kha_lastz_auto/buttons_template/CureSoliderHelp.png b/kha_lastz_auto/buttons_template/CureSoliderHelp.png new file mode 100644 index 0000000..604c8fc Binary files /dev/null and b/kha_lastz_auto/buttons_template/CureSoliderHelp.png differ diff --git a/kha_lastz_auto/buttons_template/DailyReward1.png b/kha_lastz_auto/buttons_template/DailyReward1.png new file mode 100644 index 0000000..b579499 Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyReward1.png differ diff --git a/kha_lastz_auto/buttons_template/DailyReward10.png b/kha_lastz_auto/buttons_template/DailyReward10.png new file mode 100644 index 0000000..527b04d Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyReward10.png differ diff --git a/kha_lastz_auto/buttons_template/DailyReward2.png b/kha_lastz_auto/buttons_template/DailyReward2.png new file mode 100644 index 0000000..edec9ff Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyReward2.png differ diff --git a/kha_lastz_auto/buttons_template/DailyReward3.png b/kha_lastz_auto/buttons_template/DailyReward3.png new file mode 100644 index 0000000..b78806f Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyReward3.png differ diff --git a/kha_lastz_auto/buttons_template/DailyReward4.png b/kha_lastz_auto/buttons_template/DailyReward4.png new file mode 100644 index 0000000..25f7f79 Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyReward4.png differ diff --git a/kha_lastz_auto/buttons_template/DailyReward5.png b/kha_lastz_auto/buttons_template/DailyReward5.png new file mode 100644 index 0000000..e286954 Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyReward5.png differ diff --git a/kha_lastz_auto/buttons_template/DailyReward6.png b/kha_lastz_auto/buttons_template/DailyReward6.png new file mode 100644 index 0000000..06de053 Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyReward6.png differ diff --git a/kha_lastz_auto/buttons_template/DailyReward7.png b/kha_lastz_auto/buttons_template/DailyReward7.png new file mode 100644 index 0000000..cde75e4 Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyReward7.png differ diff --git a/kha_lastz_auto/buttons_template/DailyReward8.png b/kha_lastz_auto/buttons_template/DailyReward8.png new file mode 100644 index 0000000..88068de Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyReward8.png differ diff --git a/kha_lastz_auto/buttons_template/DailyReward9.png b/kha_lastz_auto/buttons_template/DailyReward9.png new file mode 100644 index 0000000..61d900f Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyReward9.png differ diff --git a/kha_lastz_auto/buttons_template/DailyRewardClaimButton.png b/kha_lastz_auto/buttons_template/DailyRewardClaimButton.png new file mode 100644 index 0000000..360d9f4 Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyRewardClaimButton.png differ diff --git a/kha_lastz_auto/buttons_template/DailyRewardFree.png b/kha_lastz_auto/buttons_template/DailyRewardFree.png new file mode 100644 index 0000000..42eacec Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyRewardFree.png differ diff --git a/kha_lastz_auto/buttons_template/DailyShopElectric.png b/kha_lastz_auto/buttons_template/DailyShopElectric.png new file mode 100644 index 0000000..05657ba Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyShopElectric.png differ diff --git a/kha_lastz_auto/buttons_template/DailyShopGas.png b/kha_lastz_auto/buttons_template/DailyShopGas.png new file mode 100644 index 0000000..d86881a Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyShopGas.png differ diff --git a/kha_lastz_auto/buttons_template/DailyShopMaria.png b/kha_lastz_auto/buttons_template/DailyShopMaria.png new file mode 100644 index 0000000..7a9f3f4 Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyShopMaria.png differ diff --git a/kha_lastz_auto/buttons_template/DailyShopWilliam.png b/kha_lastz_auto/buttons_template/DailyShopWilliam.png new file mode 100644 index 0000000..de4e873 Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyShopWilliam.png differ diff --git a/kha_lastz_auto/buttons_template/DailyShopeIsabella.png b/kha_lastz_auto/buttons_template/DailyShopeIsabella.png new file mode 100644 index 0000000..ac701de Binary files /dev/null and b/kha_lastz_auto/buttons_template/DailyShopeIsabella.png differ diff --git a/kha_lastz_auto/buttons_template/EXPRewardFull.png b/kha_lastz_auto/buttons_template/EXPRewardFull.png new file mode 100644 index 0000000..dd0725a Binary files /dev/null and b/kha_lastz_auto/buttons_template/EXPRewardFull.png differ diff --git a/kha_lastz_auto/buttons_template/EXPRewardNormal.png b/kha_lastz_auto/buttons_template/EXPRewardNormal.png new file mode 100644 index 0000000..1a17582 Binary files /dev/null and b/kha_lastz_auto/buttons_template/EXPRewardNormal.png differ diff --git a/kha_lastz_auto/buttons_template/EventCenter.png b/kha_lastz_auto/buttons_template/EventCenter.png new file mode 100644 index 0000000..2973f16 Binary files /dev/null and b/kha_lastz_auto/buttons_template/EventCenter.png differ diff --git a/kha_lastz_auto/buttons_template/ExitGameBanner.png b/kha_lastz_auto/buttons_template/ExitGameBanner.png new file mode 100644 index 0000000..3def972 Binary files /dev/null and b/kha_lastz_auto/buttons_template/ExitGameBanner.png differ diff --git a/kha_lastz_auto/buttons_template/ExplorationReward.png b/kha_lastz_auto/buttons_template/ExplorationReward.png new file mode 100644 index 0000000..94db02e Binary files /dev/null and b/kha_lastz_auto/buttons_template/ExplorationReward.png differ diff --git a/kha_lastz_auto/buttons_template/FullGas.png b/kha_lastz_auto/buttons_template/FullGas.png new file mode 100644 index 0000000..e205a65 Binary files /dev/null and b/kha_lastz_auto/buttons_template/FullGas.png differ diff --git a/kha_lastz_auto/buttons_template/FullPreparedness.png b/kha_lastz_auto/buttons_template/FullPreparedness.png new file mode 100644 index 0000000..d1ed272 Binary files /dev/null and b/kha_lastz_auto/buttons_template/FullPreparedness.png differ diff --git a/kha_lastz_auto/buttons_template/FullPreparedness2.png b/kha_lastz_auto/buttons_template/FullPreparedness2.png new file mode 100644 index 0000000..4143d36 Binary files /dev/null and b/kha_lastz_auto/buttons_template/FullPreparedness2.png differ diff --git a/kha_lastz_auto/buttons_template/FurylordAttackIcon.png b/kha_lastz_auto/buttons_template/FurylordAttackIcon.png new file mode 100644 index 0000000..f553f53 Binary files /dev/null and b/kha_lastz_auto/buttons_template/FurylordAttackIcon.png differ diff --git a/kha_lastz_auto/buttons_template/FurylordGoButton.png b/kha_lastz_auto/buttons_template/FurylordGoButton.png new file mode 100644 index 0000000..db82384 Binary files /dev/null and b/kha_lastz_auto/buttons_template/FurylordGoButton.png differ diff --git a/kha_lastz_auto/buttons_template/FurylordIcon.png b/kha_lastz_auto/buttons_template/FurylordIcon.png new file mode 100644 index 0000000..0508fef Binary files /dev/null and b/kha_lastz_auto/buttons_template/FurylordIcon.png differ diff --git a/kha_lastz_auto/buttons_template/HeadquartersButton.png b/kha_lastz_auto/buttons_template/HeadquartersButton.png new file mode 100644 index 0000000..fe8064b Binary files /dev/null and b/kha_lastz_auto/buttons_template/HeadquartersButton.png differ diff --git a/kha_lastz_auto/buttons_template/HelpAlliance.png b/kha_lastz_auto/buttons_template/HelpAlliance.png new file mode 100644 index 0000000..f0a186e Binary files /dev/null and b/kha_lastz_auto/buttons_template/HelpAlliance.png differ diff --git a/kha_lastz_auto/buttons_template/HideTruckFromThisStateUnchecked.png b/kha_lastz_auto/buttons_template/HideTruckFromThisStateUnchecked.png new file mode 100644 index 0000000..202e0df Binary files /dev/null and b/kha_lastz_auto/buttons_template/HideTruckFromThisStateUnchecked.png differ diff --git a/kha_lastz_auto/buttons_template/KatrinaFullGas.png b/kha_lastz_auto/buttons_template/KatrinaFullGas.png new file mode 100644 index 0000000..2356dc0 Binary files /dev/null and b/kha_lastz_auto/buttons_template/KatrinaFullGas.png differ diff --git a/kha_lastz_auto/buttons_template/MagnifierButton.png b/kha_lastz_auto/buttons_template/MagnifierButton.png new file mode 100644 index 0000000..13bb7bb Binary files /dev/null and b/kha_lastz_auto/buttons_template/MagnifierButton.png differ diff --git a/kha_lastz_auto/buttons_template/Mail.png b/kha_lastz_auto/buttons_template/Mail.png new file mode 100644 index 0000000..74d5950 Binary files /dev/null and b/kha_lastz_auto/buttons_template/Mail.png differ diff --git a/kha_lastz_auto/buttons_template/March.png b/kha_lastz_auto/buttons_template/March.png new file mode 100644 index 0000000..da837ab Binary files /dev/null and b/kha_lastz_auto/buttons_template/March.png differ diff --git a/kha_lastz_auto/buttons_template/March1.png b/kha_lastz_auto/buttons_template/March1.png new file mode 100644 index 0000000..0a72926 Binary files /dev/null and b/kha_lastz_auto/buttons_template/March1.png differ diff --git a/kha_lastz_auto/buttons_template/MinusButton.png b/kha_lastz_auto/buttons_template/MinusButton.png new file mode 100644 index 0000000..23bacd5 Binary files /dev/null and b/kha_lastz_auto/buttons_template/MinusButton.png differ diff --git a/kha_lastz_auto/buttons_template/PasswordSlot.png b/kha_lastz_auto/buttons_template/PasswordSlot.png new file mode 100644 index 0000000..6d82a5a Binary files /dev/null and b/kha_lastz_auto/buttons_template/PasswordSlot.png differ diff --git a/kha_lastz_auto/buttons_template/PeaceShieldActivated.png b/kha_lastz_auto/buttons_template/PeaceShieldActivated.png new file mode 100644 index 0000000..394bc03 Binary files /dev/null and b/kha_lastz_auto/buttons_template/PeaceShieldActivated.png differ diff --git a/kha_lastz_auto/buttons_template/Plunder.png b/kha_lastz_auto/buttons_template/Plunder.png new file mode 100644 index 0000000..87017ff Binary files /dev/null and b/kha_lastz_auto/buttons_template/Plunder.png differ diff --git a/kha_lastz_auto/buttons_template/PlusButton.png b/kha_lastz_auto/buttons_template/PlusButton.png new file mode 100644 index 0000000..1a5ae1c Binary files /dev/null and b/kha_lastz_auto/buttons_template/PlusButton.png differ diff --git a/kha_lastz_auto/buttons_template/Radar1.png b/kha_lastz_auto/buttons_template/Radar1.png new file mode 100644 index 0000000..ce0aedf Binary files /dev/null and b/kha_lastz_auto/buttons_template/Radar1.png differ diff --git a/kha_lastz_auto/buttons_template/Radar2.png b/kha_lastz_auto/buttons_template/Radar2.png new file mode 100644 index 0000000..ffb0aa1 Binary files /dev/null and b/kha_lastz_auto/buttons_template/Radar2.png differ diff --git a/kha_lastz_auto/buttons_template/Radar3.png b/kha_lastz_auto/buttons_template/Radar3.png new file mode 100644 index 0000000..7eec733 Binary files /dev/null and b/kha_lastz_auto/buttons_template/Radar3.png differ diff --git a/kha_lastz_auto/buttons_template/Radar4-1.png b/kha_lastz_auto/buttons_template/Radar4-1.png new file mode 100644 index 0000000..ee4000a Binary files /dev/null and b/kha_lastz_auto/buttons_template/Radar4-1.png differ diff --git a/kha_lastz_auto/buttons_template/Radar4-2.png b/kha_lastz_auto/buttons_template/Radar4-2.png new file mode 100644 index 0000000..f85537e Binary files /dev/null and b/kha_lastz_auto/buttons_template/Radar4-2.png differ diff --git a/kha_lastz_auto/buttons_template/Radar4-3.png b/kha_lastz_auto/buttons_template/Radar4-3.png new file mode 100644 index 0000000..ec4029c Binary files /dev/null and b/kha_lastz_auto/buttons_template/Radar4-3.png differ diff --git a/kha_lastz_auto/buttons_template/Radar4-4.png b/kha_lastz_auto/buttons_template/Radar4-4.png new file mode 100644 index 0000000..c0d6a08 Binary files /dev/null and b/kha_lastz_auto/buttons_template/Radar4-4.png differ diff --git a/kha_lastz_auto/buttons_template/Radar4.png b/kha_lastz_auto/buttons_template/Radar4.png new file mode 100644 index 0000000..6cdc1da Binary files /dev/null and b/kha_lastz_auto/buttons_template/Radar4.png differ diff --git a/kha_lastz_auto/buttons_template/Radar5-1.png b/kha_lastz_auto/buttons_template/Radar5-1.png new file mode 100644 index 0000000..5f8ae87 Binary files /dev/null and b/kha_lastz_auto/buttons_template/Radar5-1.png differ diff --git a/kha_lastz_auto/buttons_template/Radar5-2 - Copy.png b/kha_lastz_auto/buttons_template/Radar5-2 - Copy.png new file mode 100644 index 0000000..f37480b Binary files /dev/null and b/kha_lastz_auto/buttons_template/Radar5-2 - Copy.png differ diff --git a/kha_lastz_auto/buttons_template/Radar5-2.png b/kha_lastz_auto/buttons_template/Radar5-2.png new file mode 100644 index 0000000..8be6011 Binary files /dev/null and b/kha_lastz_auto/buttons_template/Radar5-2.png differ diff --git a/kha_lastz_auto/buttons_template/Radar5-3.png b/kha_lastz_auto/buttons_template/Radar5-3.png new file mode 100644 index 0000000..5f04467 Binary files /dev/null and b/kha_lastz_auto/buttons_template/Radar5-3.png differ diff --git a/kha_lastz_auto/buttons_template/Radar5-4.png b/kha_lastz_auto/buttons_template/Radar5-4.png new file mode 100644 index 0000000..a35837c Binary files /dev/null and b/kha_lastz_auto/buttons_template/Radar5-4.png differ diff --git a/kha_lastz_auto/buttons_template/RadarAttack.png b/kha_lastz_auto/buttons_template/RadarAttack.png new file mode 100644 index 0000000..88e32b6 Binary files /dev/null and b/kha_lastz_auto/buttons_template/RadarAttack.png differ diff --git a/kha_lastz_auto/buttons_template/RadarFightButton.png b/kha_lastz_auto/buttons_template/RadarFightButton.png new file mode 100644 index 0000000..49e7634 Binary files /dev/null and b/kha_lastz_auto/buttons_template/RadarFightButton.png differ diff --git a/kha_lastz_auto/buttons_template/RadarHelp.png b/kha_lastz_auto/buttons_template/RadarHelp.png new file mode 100644 index 0000000..70d02f2 Binary files /dev/null and b/kha_lastz_auto/buttons_template/RadarHelp.png differ diff --git a/kha_lastz_auto/buttons_template/RadarLaura.png b/kha_lastz_auto/buttons_template/RadarLaura.png new file mode 100644 index 0000000..fc6a23f Binary files /dev/null and b/kha_lastz_auto/buttons_template/RadarLaura.png differ diff --git a/kha_lastz_auto/buttons_template/RadarRedDot.png b/kha_lastz_auto/buttons_template/RadarRedDot.png new file mode 100644 index 0000000..e1a610c Binary files /dev/null and b/kha_lastz_auto/buttons_template/RadarRedDot.png differ diff --git a/kha_lastz_auto/buttons_template/RadarTownIcon.png b/kha_lastz_auto/buttons_template/RadarTownIcon.png new file mode 100644 index 0000000..b2ccccf Binary files /dev/null and b/kha_lastz_auto/buttons_template/RadarTownIcon.png differ diff --git a/kha_lastz_auto/buttons_template/RadarZombie.png b/kha_lastz_auto/buttons_template/RadarZombie.png new file mode 100644 index 0000000..6315192 Binary files /dev/null and b/kha_lastz_auto/buttons_template/RadarZombie.png differ diff --git a/kha_lastz_auto/buttons_template/RiderCollect.png b/kha_lastz_auto/buttons_template/RiderCollect.png new file mode 100644 index 0000000..acb5eef Binary files /dev/null and b/kha_lastz_auto/buttons_template/RiderCollect.png differ diff --git a/kha_lastz_auto/buttons_template/SPlunderTruckSmallBanner.png b/kha_lastz_auto/buttons_template/SPlunderTruckSmallBanner.png new file mode 100644 index 0000000..0165bef Binary files /dev/null and b/kha_lastz_auto/buttons_template/SPlunderTruckSmallBanner.png differ diff --git a/kha_lastz_auto/buttons_template/SearchButton.png b/kha_lastz_auto/buttons_template/SearchButton.png new file mode 100644 index 0000000..9c7efb3 Binary files /dev/null and b/kha_lastz_auto/buttons_template/SearchButton.png differ diff --git a/kha_lastz_auto/buttons_template/ShieldConfirmButton.png b/kha_lastz_auto/buttons_template/ShieldConfirmButton.png new file mode 100644 index 0000000..d1ae5bf Binary files /dev/null and b/kha_lastz_auto/buttons_template/ShieldConfirmButton.png differ diff --git a/kha_lastz_auto/buttons_template/ShooterCollect.png b/kha_lastz_auto/buttons_template/ShooterCollect.png new file mode 100644 index 0000000..eb8bbb9 Binary files /dev/null and b/kha_lastz_auto/buttons_template/ShooterCollect.png differ diff --git a/kha_lastz_auto/buttons_template/Slider.png b/kha_lastz_auto/buttons_template/Slider.png new file mode 100644 index 0000000..f766427 Binary files /dev/null and b/kha_lastz_auto/buttons_template/Slider.png differ diff --git a/kha_lastz_auto/buttons_template/SoliderTrain.png b/kha_lastz_auto/buttons_template/SoliderTrain.png new file mode 100644 index 0000000..8305c28 Binary files /dev/null and b/kha_lastz_auto/buttons_template/SoliderTrain.png differ diff --git a/kha_lastz_auto/buttons_template/SoliderTrainButton.png b/kha_lastz_auto/buttons_template/SoliderTrainButton.png new file mode 100644 index 0000000..e5faba8 Binary files /dev/null and b/kha_lastz_auto/buttons_template/SoliderTrainButton.png differ diff --git a/kha_lastz_auto/buttons_template/SystemButton.png b/kha_lastz_auto/buttons_template/SystemButton.png new file mode 100644 index 0000000..1ff60b8 Binary files /dev/null and b/kha_lastz_auto/buttons_template/SystemButton.png differ diff --git a/kha_lastz_auto/buttons_template/TeamUp.png b/kha_lastz_auto/buttons_template/TeamUp.png new file mode 100644 index 0000000..2f5650d Binary files /dev/null and b/kha_lastz_auto/buttons_template/TeamUp.png differ diff --git a/kha_lastz_auto/buttons_template/Treasure1-1.png b/kha_lastz_auto/buttons_template/Treasure1-1.png new file mode 100644 index 0000000..213e8ce Binary files /dev/null and b/kha_lastz_auto/buttons_template/Treasure1-1.png differ diff --git a/kha_lastz_auto/buttons_template/Treasure1-2.png b/kha_lastz_auto/buttons_template/Treasure1-2.png new file mode 100644 index 0000000..cfa8793 Binary files /dev/null and b/kha_lastz_auto/buttons_template/Treasure1-2.png differ diff --git a/kha_lastz_auto/buttons_template/Treasure1-3.png b/kha_lastz_auto/buttons_template/Treasure1-3.png new file mode 100644 index 0000000..b86ed2d Binary files /dev/null and b/kha_lastz_auto/buttons_template/Treasure1-3.png differ diff --git a/kha_lastz_auto/buttons_template/Treasure1.png b/kha_lastz_auto/buttons_template/Treasure1.png new file mode 100644 index 0000000..440a967 Binary files /dev/null and b/kha_lastz_auto/buttons_template/Treasure1.png differ diff --git a/kha_lastz_auto/buttons_template/Treasure2.png b/kha_lastz_auto/buttons_template/Treasure2.png new file mode 100644 index 0000000..bf0d136 Binary files /dev/null and b/kha_lastz_auto/buttons_template/Treasure2.png differ diff --git a/kha_lastz_auto/buttons_template/Treasure3.png b/kha_lastz_auto/buttons_template/Treasure3.png new file mode 100644 index 0000000..0867a21 Binary files /dev/null and b/kha_lastz_auto/buttons_template/Treasure3.png differ diff --git a/kha_lastz_auto/buttons_template/TreasureTest.png b/kha_lastz_auto/buttons_template/TreasureTest.png new file mode 100644 index 0000000..b308afa Binary files /dev/null and b/kha_lastz_auto/buttons_template/TreasureTest.png differ diff --git a/kha_lastz_auto/buttons_template/TruckIcon.png b/kha_lastz_auto/buttons_template/TruckIcon.png new file mode 100644 index 0000000..e89defd Binary files /dev/null and b/kha_lastz_auto/buttons_template/TruckIcon.png differ diff --git a/kha_lastz_auto/buttons_template/TruckLootButton.png b/kha_lastz_auto/buttons_template/TruckLootButton.png new file mode 100644 index 0000000..c2d1a7a Binary files /dev/null and b/kha_lastz_auto/buttons_template/TruckLootButton.png differ diff --git a/kha_lastz_auto/buttons_template/TruckPlunderAlliancePoint.png b/kha_lastz_auto/buttons_template/TruckPlunderAlliancePoint.png new file mode 100644 index 0000000..6aca1db Binary files /dev/null and b/kha_lastz_auto/buttons_template/TruckPlunderAlliancePoint.png differ diff --git a/kha_lastz_auto/buttons_template/TruckPlunderDiamon.png b/kha_lastz_auto/buttons_template/TruckPlunderDiamon.png new file mode 100644 index 0000000..789e89b Binary files /dev/null and b/kha_lastz_auto/buttons_template/TruckPlunderDiamon.png differ diff --git a/kha_lastz_auto/buttons_template/TruckPlunderDispatchOrder.png b/kha_lastz_auto/buttons_template/TruckPlunderDispatchOrder.png new file mode 100644 index 0000000..c35b00f Binary files /dev/null and b/kha_lastz_auto/buttons_template/TruckPlunderDispatchOrder.png differ diff --git a/kha_lastz_auto/buttons_template/TruckPlunderExteriorModule.png b/kha_lastz_auto/buttons_template/TruckPlunderExteriorModule.png new file mode 100644 index 0000000..f6f6d6b Binary files /dev/null and b/kha_lastz_auto/buttons_template/TruckPlunderExteriorModule.png differ diff --git a/kha_lastz_auto/buttons_template/TruckPlunderOrangeHeroFragment.png b/kha_lastz_auto/buttons_template/TruckPlunderOrangeHeroFragment.png new file mode 100644 index 0000000..432d4a9 Binary files /dev/null and b/kha_lastz_auto/buttons_template/TruckPlunderOrangeHeroFragment.png differ diff --git a/kha_lastz_auto/buttons_template/TruckPlunderPrimeRecuirtment.png b/kha_lastz_auto/buttons_template/TruckPlunderPrimeRecuirtment.png new file mode 100644 index 0000000..2c786f6 Binary files /dev/null and b/kha_lastz_auto/buttons_template/TruckPlunderPrimeRecuirtment.png differ diff --git a/kha_lastz_auto/buttons_template/TruckRefresh.png b/kha_lastz_auto/buttons_template/TruckRefresh.png new file mode 100644 index 0000000..27f0045 Binary files /dev/null and b/kha_lastz_auto/buttons_template/TruckRefresh.png differ diff --git a/kha_lastz_auto/buttons_template/Use8HoursPeaceShield.png b/kha_lastz_auto/buttons_template/Use8HoursPeaceShield.png new file mode 100644 index 0000000..39c75dc Binary files /dev/null and b/kha_lastz_auto/buttons_template/Use8HoursPeaceShield.png differ diff --git a/kha_lastz_auto/buttons_template/VivianClaimAllDailyReward.png b/kha_lastz_auto/buttons_template/VivianClaimAllDailyReward.png new file mode 100644 index 0000000..61e5d47 Binary files /dev/null and b/kha_lastz_auto/buttons_template/VivianClaimAllDailyReward.png differ diff --git a/kha_lastz_auto/buttons_template/WorldButton.png b/kha_lastz_auto/buttons_template/WorldButton.png new file mode 100644 index 0000000..6680c4a Binary files /dev/null and b/kha_lastz_auto/buttons_template/WorldButton.png differ diff --git a/kha_lastz_auto/buttons_template/YellowTruckSmall2.png b/kha_lastz_auto/buttons_template/YellowTruckSmall2.png new file mode 100644 index 0000000..21d2692 Binary files /dev/null and b/kha_lastz_auto/buttons_template/YellowTruckSmall2.png differ diff --git a/kha_lastz_auto/buttons_template/YellowTruckSmall3.png b/kha_lastz_auto/buttons_template/YellowTruckSmall3.png new file mode 100644 index 0000000..a9e46ed Binary files /dev/null and b/kha_lastz_auto/buttons_template/YellowTruckSmall3.png differ diff --git a/kha_lastz_auto/buttons_template/ZGoldNormal.png b/kha_lastz_auto/buttons_template/ZGoldNormal.png new file mode 100644 index 0000000..f8e692d Binary files /dev/null and b/kha_lastz_auto/buttons_template/ZGoldNormal.png differ diff --git a/kha_lastz_auto/config.yaml b/kha_lastz_auto/config.yaml new file mode 100644 index 0000000..20d89d7 --- /dev/null +++ b/kha_lastz_auto/config.yaml @@ -0,0 +1,162 @@ +# Kich thuoc client area cua so game luc chup template images (resolution cua template). +# Vision scale = window_width / reference_width. Khong thay doi tru khi chup lai template. +reference_width: 1080 +reference_height: 1920 + +# Kich thuoc cua so game muon resize ve khi khoi dong bot. +# De trong (hoac xoa 2 dong nay) neu khong muon bot tu resize window. +# Vi du: chay game o 540x960 de giam tai CPU trong khi template chup o 1080x1920. +window_width: 540 +window_height: 960 +show_preview: false # live OpenCV preview; overridden by App settings → .env_config general_settings +auto_focus: false # tự động chiếm quyền focus khi click (mặc định tắt) + +# Min seconds between screenshot captures in the main loop (0.02–2), or set FPS from UI (stored as 1/FPS). +# Example: 0.1 s ≈ 10 FPS cap. +capture_interval_sec: 0.1 + +# Emulator selection: "pc" (default, LastZ PC client) or "ldplayer" (LDPlayer emulator). +# - pc: connects to the "LastZ" window; screenshots via win32 + mss. +# - ldplayer: connects to the "LDPlayer" window; screenshots via ADB (adb exec-out screencap -p). +emulator: pc + +# Auto start LastZ (only when emulator: pc). +# When true, the bot will auto-launch LastZ.exe if the process is not running. +auto_start_lastz: false + +# Path to LDPlayer's adb.exe (only used when emulator: ldplayer). +# Defaults to C:\LDPlayer\LDPlayer9\adb.exe if not set. +# ldplayer_adb_path: C:\LDPlayer\LDPlayer9\adb.exe + +# ADB device serial for LDPlayer (only used when emulator: ldplayer). +# Leave commented-out (or remove) to enable auto-detection on startup: +# 1. Checks 'adb devices' for an already-connected device. +# 2. If none found, probes 127.0.0.1:5555, :5557, :5559 … (one port per LDPlayer instance). +# Set manually only if auto-detect picks the wrong device (e.g. multiple emulators running). +# Example: ldplayer_device_serial: 127.0.0.1:5555 +# ldplayer_device_serial: + +# Function list: key, cron, enabled, cooldown (seconds; for trigger-based functions, skip re-trigger within cooldown) +# https://crontab.guru/ for cron syntax +# Functions run in FIFO order: whoever triggers first runs first. +# If a function triggers while another is running, it is queued and runs after. +functions: + - name: PinLoggin + trigger: logged_out + enabled: true + key: l + cooldown: 50 + + - name: BeingAttacked + trigger: attacked + enabled: true + key: p + - name: TestAllianceAttack + trigger: alliance_attacked + enabled: true + key: a + - name: TurnOnShield + trigger: attacked + key: o + enabled: true + + - name: FightBoomer + key: b + cron: "*/3 * * * *" + enabled: false + + - name: AttackFuryLord + enabled: true + + - name: AttackArena + enabled: true + + - name: ClickTreasure + trigger: treasure_detected + key: y + enabled: true + cooldown: 60 + + - name: ClickTreasureManual + key: t + enabled: true + + - name: DoRadarTask + enabled: true + + - name: DoBountyMission + enabled: true + + - name: CollectDailyShopReward + enabled: true + + - name: CollectFullPreparednessReward + enabled: true + + - name: CollectDailyReward + enabled: true + + + - name: CollectZGoldFull + enabled: true + key: z + + - name: CollectMiaBluePrintFull + enabled: true + + - name: CollectVivianDailyReward + enabled: true + + - name: CollectKatrinaGasFull + enabled: true + + - name: CollectExplorationReward + cron: "0 */2 * * *" + enabled: true + key: c + + - name: SoliderTrain + cron: "0 */3 * * *" + enabled: true + key: s + + - name: HelpAlliance + cron: "*/2 * * * *" + enabled: true + key: h + + - name: CheckMail + key: m + cron: "0 */5 * * *" + enabled: true + + - name: DonateAllianceTech + key: d + cron: "* * * * *" + enabled: true + + + - name: TruckPlunder + enabled: true + key: x + +# If true (Windows only), abort the running YAML function when the user does a +# horizontal zoom-shake (left then right, like macOS zoom), not plain fast movement. +# Override in .env_config under general_settings if needed. +abort_on_fast_user_mouse: true +# Default: poll GetCursorPos — no WH_MOUSE_LL, smooth drag. +# fast_user_mouse_use_low_level_hook: true # WH_MOUSE_LL; ignores injected input; can stutter on drag +# fast_user_mouse_poll_interval_sec: 0.008 +# fast_user_mouse_gesture_left_leg_px: 75 # min px toward smaller x before reversing +# fast_user_mouse_gesture_right_leg_px: 75 # min px back toward larger x from trough +# fast_user_mouse_gesture_window_sec: 0.42 # sliding time window for the gesture +# fast_user_mouse_gesture_max_downstroke_sec: 0.22 # max time high→trough (0 = no cap) +# fast_user_mouse_gesture_max_upstroke_sec: 0.22 # max time trough→recovery (0 = no cap) +# fast_user_mouse_gesture_min_leg_balance_ratio: 0.4 # min(shorter/longer leg); rejects one-way drags +# fast_user_mouse_gesture_max_single_leg_px: 400 # cap either leg (0 = no cap) +# Legacy: fast_user_mouse_window_sec / fast_user_mouse_window_min_dist_px still apply if gesture keys omitted +# fast_user_mouse_cooldown_sec: 0.45 +# fast_user_mouse_count_all_moves_as_physical: false # hook mode only; bot may trip if true + + + \ No newline at end of file diff --git a/kha_lastz_auto/config_manager.py b/kha_lastz_auto/config_manager.py new file mode 100644 index 0000000..06880b1 --- /dev/null +++ b/kha_lastz_auto/config_manager.py @@ -0,0 +1,218 @@ +""" +config_manager.py +----------------- +Manages .env_config (YAML) — user overrides for config.yaml. + +Load priority: config.yaml → .env_config (override) +Saves: key bindings, enabled states, cron overrides. + +Format: + key_bindings: + PinLoggin: l + FightBoomer: "" # empty = no hotkey + cron_overrides: + HelpAlliance: "*/5 * * * *" + fn_enabled: + PinLoggin: true + FightBoomer: false +""" + +import logging +import os +import re +import sys + +import yaml + +_log = logging.getLogger("kha_lastz") + +ENV_CONFIG_PATH = ".env_config" + +# More than one top-level `general_settings:` is a config error: PyYAML keeps only the last +# mapping, so earlier keys are dropped silently. +_TOP_LEVEL_GENERAL_SETTINGS = re.compile(r"^general_settings:\s*$", re.MULTILINE) + + +def _env_config_abort(message: str) -> None: + """Log a fatal .env_config error and exit the process.""" + _log.critical("[config_manager] .env_config — %s Fix the file and restart.", message) + sys.exit(1) + + +def _check_duplicate_top_level_general_settings(text: str, abs_path: str) -> None: + matches = list(_TOP_LEVEL_GENERAL_SETTINGS.finditer(text)) + if len(matches) <= 1: + return + lines = [text[: m.start()].count("\n") + 1 for m in matches] + _env_config_abort( + "invalid structure: multiple top-level 'general_settings:' keys at lines {} " + "(PyYAML only keeps the last block; merge into one section). Path: {}".format( + ", ".join(str(n) for n in lines), + abs_path, + ) + ) + + +def load_env_config() -> dict: + """Load .env_config as a dict. Exits the process on YAML syntax errors or invalid structure.""" + if not os.path.isfile(ENV_CONFIG_PATH): + return {} + abs_path = os.path.abspath(ENV_CONFIG_PATH) + try: + with open(ENV_CONFIG_PATH, "r", encoding="utf-8") as f: + text = f.read() + except OSError as exc: + _env_config_abort("cannot read file: {} ({})".format(abs_path, exc)) + + _check_duplicate_top_level_general_settings(text, abs_path) + + try: + data = yaml.safe_load(text) + except yaml.YAMLError as exc: + _log.critical( + "[config_manager] .env_config YAML syntax error in %s —\n%s\nFix the file and restart.", + abs_path, + exc, + ) + sys.exit(1) + + if data is None: + return {} + if not isinstance(data, dict): + _env_config_abort( + "expected a YAML mapping (key: value) at the root, got {!r}. Path: {}".format( + type(data).__name__, + abs_path, + ) + ) + return data + + +def load_general_settings() -> dict: + """Return the general_settings mapping from .env_config, or {} if missing/unreadable.""" + gs = load_env_config().get("general_settings") + return dict(gs) if isinstance(gs, dict) else {} + + +def apply_overrides(fn_configs: list) -> None: + """Load .env_config and apply key_bindings, cron_overrides, fn_enabled to fn_configs in-place. + Must be called BEFORE the loop that builds key_bindings / fn_enabled in main.py. + """ + abs_path = os.path.abspath(ENV_CONFIG_PATH) + if not os.path.isfile(ENV_CONFIG_PATH): + _log.warning("[config_manager] .env_config not found at %s — using defaults", abs_path) + return {} + env = load_env_config() + + kb_ov = env.get("key_bindings", {}) + cr_ov = env.get("cron_overrides", {}) + en_ov = env.get("fn_enabled", {}) + gs_ov = env.get("general_settings") or {} + _log.info( + "[config_manager] apply_overrides: loaded general_settings from %s → %s", + abs_path, gs_ov, + ) + + for fc in fn_configs: + name = fc.get("name") + if not name: + continue + if name in kb_ov: + fc["key"] = kb_ov[name] or "" + if name in cr_ov: + if cr_ov[name]: + fc["cron"] = cr_ov[name] + else: + fc.pop("cron", None) + if name in en_ov: + fc["enabled"] = bool(en_ov[name]) + + return gs_ov + + +def save(fn_configs: list, fn_enabled: dict, general_settings: dict = None) -> None: + """Persist key bindings, cron overrides, and enabled states to .env_config. + Preserves all other sections (fn_settings, etc.) already in the file.""" + # Read existing file to preserve sections we don't manage here (e.g. fn_settings) + if os.path.isfile(ENV_CONFIG_PATH): + data = load_env_config() + else: + data = {} + + kb = {} + cr = {} + for fc in fn_configs: + name = fc.get("name") + if not name: + continue + kb[name] = fc.get("key") or "" + cron = fc.get("cron", "") + if cron: + cr[name] = cron + + data["key_bindings"] = kb + data["cron_overrides"] = cr + data["fn_enabled"] = {k: bool(v) for k, v in fn_enabled.items()} + if general_settings is not None: + # Merge rather than replace: preserve any keys already in the file that + # are not managed by the app (e.g. fast_user_mouse_* tuning values added + # manually). Managed keys in general_settings always win. + existing_gs = data.get("general_settings") or {} + merged_gs = dict(existing_gs) + merged_gs.update(general_settings) + data["general_settings"] = merged_gs + _log.info( + "[config_manager] save: writing general_settings → %s", + {k: v for k, v in merged_gs.items() if k in ( + "window_width", "window_height", "language", "auto_start_lastz", + "capture_interval_sec", "show_preview", "auto_focus", + )}, + ) + + with open(ENV_CONFIG_PATH, "w", encoding="utf-8") as f: + yaml.dump(data, f, allow_unicode=True, default_flow_style=False, sort_keys=False) + + +def load_fn_settings() -> dict: + """Return fn_settings dict {fn_name: {key: value}} from .env_config.""" + if not os.path.isfile(ENV_CONFIG_PATH): + return {} + data = load_env_config() + return data.get("fn_settings") or {} + + +def save_fn_settings(fn_settings: dict) -> None: + """Persist fn_settings into the fn_settings section of .env_config.""" + if os.path.isfile(ENV_CONFIG_PATH): + data = load_env_config() + else: + data = {} + data["fn_settings"] = fn_settings + with open(ENV_CONFIG_PATH, "w", encoding="utf-8") as f: + yaml.dump(data, f, allow_unicode=True, default_flow_style=False, sort_keys=False) + + +def init_if_missing(fn_configs: list, fn_enabled: dict) -> None: + """Create .env_config if missing, or add any absent cron_overrides from fn_configs.""" + if not os.path.isfile(ENV_CONFIG_PATH): + save(fn_configs, fn_enabled) + return + + # File exists — backfill cron_overrides for functions that have cron in + # fn_configs but are not yet recorded in the file (e.g. file was created + # before cron support was added). + data = load_env_config() + + cr = data.get("cron_overrides") or {} + changed = False + for fc in fn_configs: + name = fc.get("name") + if name and fc.get("cron") and name not in cr: + cr[name] = fc["cron"] + changed = True + + if changed: + data["cron_overrides"] = cr + with open(ENV_CONFIG_PATH, "w", encoding="utf-8") as f: + yaml.dump(data, f, allow_unicode=True, + default_flow_style=False, sort_keys=False) diff --git a/kha_lastz_auto/connection_detector.py b/kha_lastz_auto/connection_detector.py new file mode 100644 index 0000000..4c886cf --- /dev/null +++ b/kha_lastz_auto/connection_detector.py @@ -0,0 +1,381 @@ +""" +Connection detector: every N minutes, check if the game is still connected. + +Runs only when Is Running (UI) is true — same as all other detectors; the detector +loop in main skips all detectors when bot is paused. + +- If LastZ process is not running → start it and refresh window handle/PID. +- If process is running → run world_zoomout (same as event), sleep 2, click middle, + sleep 2, check BuffIcon. If BuffIcon not found → assume disconnected: kill process + and restart LastZ. +""" + +import os +import time +import subprocess +import ctypes + +from vision import get_global_scale +from zoom_helpers import do_world_zoomout, do_base_zoomout + + +# ── Timing ──────────────────────────────────────────────────────────────────── +WAIT_BEFORE_START_SEC: float = 10.0 # wait before starting LastZ after process dies +WAIT_AFTER_START_SEC: float = 10.0 # wait after starting LastZ for window to appear +RECENTLY_STARTED_GUARD_SEC: float = 60.0 # skip re-start if process was started within this many seconds +KILL_WAIT_SEC: float = 2.0 # wait after killing process before starting +FOCUS_SLEEP_SEC: float = 0.2 # sleep after focusing window before check +AFTER_ZOOMOUT_SLEEP_SEC: float = 2.0 # sleep after world_zoomout before clicking middle +AFTER_CLICK_SLEEP_SEC: float = 2.0 # sleep after clicking middle before screenshot + +# ── BuffIcon check ──────────────────────────────────────────────────────────── +BUFF_MISS_RECHECK_SEC: float = 60 # re-check delay (seconds) after first BuffIcon miss +BUFF_MISSES_BEFORE_RESTART: int = 3 # consecutive misses required before killing and restarting + +# ── Vision thresholds ───────────────────────────────────────────────────────── +THRESHOLD_DEFAULT: float = 0.75 # default template-match threshold + +# ── close_ui ────────────────────────────────────────────────────────────────── +CLOSE_UI_CLICK_X: float = 0.03 # relative X position of the dismiss-tap (fraction of window width) +CLOSE_UI_CLICK_Y: float = 0.08 # relative Y position of the dismiss-tap +CLOSE_UI_MAX_TRIES: int = 10 # max dismiss attempts before giving up +CLOSE_UI_CLICK_SLEEP_SEC: float = 1.0 # sleep after each dismiss tap +CLOSE_UI_FOCUS_SLEEP_SEC: float = 0.05 # sleep after refocusing window inside close_ui loop +CLOSE_UI_POST_CLICK_SLEEP_SEC: float = 0.3 # sleep after refreshing screenshot inside close_ui loop + +# ── world_zoomout ───────────────────────────────────────────────────────────── +WORLD_ZOOMOUT_SCROLL_TIMES: int = 0 +WORLD_ZOOMOUT_SCROLL_INTERVAL_SEC: float = 0.1 +WORLD_ZOOMOUT_ROI_CENTER_X: float = 0.93 +WORLD_ZOOMOUT_ROI_CENTER_Y: float = 0.96 +WORLD_ZOOMOUT_ROI_PADDING: int = 2 + +# ── base_zoomout (run after connection OK) ──────────────────────────────────── +BASE_ZOOMOUT_SCROLL_TIMES: int = 5 +BASE_ZOOMOUT_SCROLL_INTERVAL_SEC: float = 0.1 + +# ── taskkill ────────────────────────────────────────────────────────────────── +TASKKILL_TIMEOUT_SEC: int = 10 # timeout (seconds) for the taskkill command + + +# Windows: check if process exists by opening handle +def _is_process_running(pid): + if pid is None or pid <= 0: + return False + try: + # PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 + h = ctypes.windll.kernel32.OpenProcess(0x1000, False, pid) + if h: + ctypes.windll.kernel32.CloseHandle(h) + return True + except Exception: + pass + return False + + +def _kill_process(pid, log): + """Terminate process by PID. On Windows uses taskkill.""" + if pid is None or pid <= 0: + return + try: + subprocess.run( + ["taskkill", "/F", "/PID", str(pid)], + capture_output=True, + timeout=TASKKILL_TIMEOUT_SEC, + ) + log.info("[ConnectionDetector] Killed LastZ process PID=%s", pid) + except Exception as e: + log.warning("[ConnectionDetector] taskkill failed: %s", e) + + +def _start_lastz(exe_path, log): + """Start LastZ process. Returns True on success.""" + if not exe_path or not os.path.isfile(exe_path): + log.error("[ConnectionDetector] LastZ exe not found: %s", exe_path) + return False + try: + cwd = os.path.dirname(exe_path) + subprocess.Popen( + [exe_path], + cwd=cwd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + log.info("[ConnectionDetector] Started LastZ: %s", exe_path) + return True + except Exception as e: + log.error("[ConnectionDetector] Failed to start LastZ: %s", e) + return False + + +def _find_window_and_pid(window_name): + """Find window by title; return (hwnd, pid) or (None, None).""" + try: + import win32gui + import win32process + hwnd = win32gui.FindWindow(None, window_name) + if not hwnd or not win32gui.IsWindow(hwnd): + return None, None + _, pid = win32process.GetWindowThreadProcessId(hwnd) + return hwnd, pid + except Exception: + return None, None + + +def _do_close_ui(wincap, vision_cache, template_path, world_button_path, log, + threshold=THRESHOLD_DEFAULT, click_x=CLOSE_UI_CLICK_X, + click_y=CLOSE_UI_CLICK_Y, max_tries=CLOSE_UI_MAX_TRIES): + """ + Close overlays until HQ or World is visible (same logic as close_ui step). + Loop: if template or world_button found in screenshot, return True; else click at (click_x, click_y), sleep, refresh, retry. + """ + vision = vision_cache.get(template_path) if template_path else None + vision_world = vision_cache.get(world_button_path) if world_button_path else None + if not vision and not vision_world: + log.warning("[ConnectionDetector] close_ui: no template in cache") + return False + scr = wincap.get_screenshot() + if scr is None: + return False + for _try in range(max_tries): + phq = vision.find(scr, threshold=threshold) if vision else [] + pw = vision_world.find(scr, threshold=threshold) if vision_world else [] + if (phq and len(phq) > 0) or (pw and len(pw) > 0): + log.info("[ConnectionDetector] close_ui → true (HQ/World visible after %d click(s))", _try) + return True + if _try < max_tries - 1: + if hasattr(wincap, "focus_window"): + wincap.focus_window(force=True) + time.sleep(CLOSE_UI_FOCUS_SLEEP_SEC) + px = int(wincap.w * click_x) + py = int(wincap.h * click_y) + from zoom_helpers import _tap_game_pixel + + _tap_game_pixel(wincap, px, py) + time.sleep(CLOSE_UI_CLICK_SLEEP_SEC) + fresh = wincap.get_screenshot() + if fresh is not None: + scr = fresh + time.sleep(CLOSE_UI_POST_CLICK_SLEEP_SEC) + log.warning("[ConnectionDetector] close_ui → false (max_tries=%d)", max_tries) + return False + + +class ConnectionDetector: + """ + Every `interval_sec` seconds (e.g. 300 = 5 min): + - If LastZ process is not running → start exe and refresh hwnd/pid. + - Else: world_zoomout, sleep 2, click middle, sleep 2, check BuffIcon. + If BuffIcon not found → kill process and restart LastZ. + """ + + def __init__( + self, + buff_icon_template_path: str, + world_zoomout_template_path: str, + world_zoomout_button_path: str, + lastz_exe_path: str, + lastz_pid_ref: dict, + lastz_window_name: str = "LastZ", + interval_sec: float = 1800.0, + buff_icon_threshold: float = 0.75, + autostart_state_ref: dict | None = None, + ): + self._buff_icon_path = buff_icon_template_path + self._world_template = world_zoomout_template_path + self._world_button = world_zoomout_button_path + self._lastz_exe = lastz_exe_path + self._lastz_pid_ref = lastz_pid_ref # mutable: {"pid": int} + self._window_name = lastz_window_name + self._interval_sec = interval_sec + self._buff_threshold = buff_icon_threshold + self._last_run_time = time.time() # first check after interval_sec from startup + self._last_start_time: float = 0.0 # timestamp of last _start_lastz call + self._buff_miss_count: int = 0 # consecutive BuffIcon-not-found; restart at BUFF_MISSES_BEFORE_RESTART + self._busy_deferred_log_t: float = 0.0 # throttle "deferred" log while busy + # Shared with maybe_autostart_lastz so both systems respect the same cooldown. + # When connection_detector starts LastZ, it stamps this dict so the watcher loop + # won't start a second copy while the game is still loading. + self._autostart_state_ref: dict = autostart_state_ref if autostart_state_ref is not None else {} + + def _stamp_autostart(self) -> None: + """Stamp the shared autostart-state dict so maybe_autostart_lastz won't fire a second start.""" + self._autostart_state_ref["t"] = time.time() + + def reset(self): + """On resume: next full check in interval_sec (e.g. 5 min), not immediately.""" + self._last_run_time = time.time() + + def should_run(self, now_ts): + """Return True if interval has elapsed since last run.""" + if self._last_run_time <= 0: + return True + return (now_ts - self._last_run_time) >= self._interval_sec + + def update(self, wincap, vision_cache, log, current_screenshot=None, is_busy=None): + """ + Call every detector tick. Process check runs every tick (restart LastZ soon after window closed). + Full connection check (close_ui, world_zoomout, BuffIcon) runs every interval_sec (e.g. 5 min). + When current_screenshot is provided and PasswordSlot is visible (login screen), skip full check. + + Args: + is_busy: optional callable returning True while a bot function is actively running. + Only the full connection check is deferred when busy; process check and + window reattach always run so LastZ is recovered even during a bot function. + """ + if getattr(wincap, "is_using_adb", False): + # LDPlayer pure-ADB mode: no Win32 window / LastZ.exe lifecycle here. + return + + now = time.time() + busy = is_busy and is_busy() + + pid = self._lastz_pid_ref.get("pid") + + # 1) Every tick: if process not running → wait 10s then start and refresh hwnd/pid (so closing window triggers restart soon) + if not _is_process_running(pid): + # Guard: if we started the process recently, just try to reattach the window + # instead of starting again (prevents double-start when window hasn't appeared yet) + elapsed_since_start = now - self._last_start_time + if self._last_start_time > 0 and elapsed_since_start < RECENTLY_STARTED_GUARD_SEC: + log.debug( + "[ConnectionDetector] Process recently started (%.0fs ago), waiting for window...", + elapsed_since_start, + ) + hwnd, new_pid = _find_window_and_pid(self._window_name) + if hwnd is not None: + wincap.hwnd = hwnd + wincap.refresh_geometry() + self._lastz_pid_ref["pid"] = new_pid + log.info("[ConnectionDetector] Window reattached, PID=%s", new_pid) + return + + log.info("[ConnectionDetector] LastZ process not running (PID=%s), waiting %.0fs then starting...", pid, WAIT_BEFORE_START_SEC) + time.sleep(WAIT_BEFORE_START_SEC) + self._last_start_time = now + self._stamp_autostart() + if _start_lastz(self._lastz_exe, log): + time.sleep(WAIT_AFTER_START_SEC) + hwnd, new_pid = _find_window_and_pid(self._window_name) + if hwnd is not None: + wincap.hwnd = hwnd + wincap.refresh_geometry() + self._lastz_pid_ref["pid"] = new_pid + log.info("[ConnectionDetector] Window reattached, PID=%s", new_pid) + return + + # 1.5) Process running but caller had no screenshot (e.g. detector screenshot failed) — try to re-find window in case hwnd is stale + if current_screenshot is None: + hwnd, new_pid = _find_window_and_pid(self._window_name) + if hwnd is not None: + try: + import win32gui + if not win32gui.IsWindow(getattr(wincap, "hwnd", None)) or wincap.hwnd != hwnd: + wincap.hwnd = hwnd + wincap.refresh_geometry() + self._lastz_pid_ref["pid"] = new_pid + log.info("[ConnectionDetector] Window reattached (hwnd was invalid), PID=%s", new_pid) + except Exception: + pass + return # no screenshot available, skip full check + + # 2) Full connection check only every interval_sec (e.g. 5 min) + if not self.should_run(now): + return + + # Guard: skip full connection check while any bot function is actively running. + # Do NOT reset _last_run_time here — the check remains "due" and fires as soon as idle. + if busy: + if now - self._busy_deferred_log_t >= 30.0: + log.debug("[ConnectionDetector] Check due but deferred — bot function is running") + self._busy_deferred_log_t = now + return + # Skip connection check when on login screen (PasswordSlot visible) + if current_screenshot is not None: + try: + _pv = vision_cache.get("buttons_template/PasswordSlot.png") + if _pv and _pv.exists(current_screenshot, threshold=THRESHOLD_DEFAULT): + log.info("[ConnectionDetector] On login screen, skipping check") + self._last_run_time = now + return + except Exception: + pass + self._last_run_time = now + + # 3) Process is running: run connection check (close_ui → world_zoomout → click → BuffIcon) + if hasattr(wincap, "focus_window"): + wincap.focus_window(force=True) + time.sleep(FOCUS_SLEEP_SEC) + _do_close_ui( + wincap, vision_cache, + self._world_template, self._world_button, log, + ) + if not do_world_zoomout( + wincap, vision_cache, log, + self._world_template, self._world_button, + screenshot=None, + threshold=THRESHOLD_DEFAULT, + scroll_times=WORLD_ZOOMOUT_SCROLL_TIMES, + scroll_interval_sec=WORLD_ZOOMOUT_SCROLL_INTERVAL_SEC, + roi_center_x=WORLD_ZOOMOUT_ROI_CENTER_X, + roi_center_y=WORLD_ZOOMOUT_ROI_CENTER_Y, + roi_padding=WORLD_ZOOMOUT_ROI_PADDING, + log_prefix="[ConnectionDetector] ", + ): + log.warning("[ConnectionDetector] world_zoomout failed (not on world?), continuing check") + time.sleep(AFTER_ZOOMOUT_SLEEP_SEC) + # Click middle of the game surface (client / device pixels) + cx = wincap.w // 2 + cy = wincap.h // 2 + from zoom_helpers import _tap_game_pixel + + _tap_game_pixel(wincap, cx, cy) + time.sleep(AFTER_CLICK_SLEEP_SEC) + screenshot = wincap.get_screenshot() + if screenshot is None: + log.warning("[ConnectionDetector] Screenshot failed after click") + return + buff_vision = vision_cache.get(self._buff_icon_path) + if not buff_vision: + log.warning("[ConnectionDetector] BuffIcon template not in cache: %s", self._buff_icon_path) + return + if buff_vision.exists(screenshot, threshold=self._buff_threshold): + log.info("[ConnectionDetector] BuffIcon found → connection OK") + self._buff_miss_count = 0 + # Run base_zoomout after connection OK (zoom out to world view) + do_base_zoomout( + wincap, vision_cache, log, + self._world_template, self._world_button, + screenshot=None, + threshold=THRESHOLD_DEFAULT, + scroll_times=BASE_ZOOMOUT_SCROLL_TIMES, + scroll_interval_sec=BASE_ZOOMOUT_SCROLL_INTERVAL_SEC, + log_prefix="[ConnectionDetector] ", + ) + return + + # BuffIcon not found — require BUFF_MISSES_BEFORE_RESTART consecutive misses before restarting + self._buff_miss_count += 1 + if self._buff_miss_count < BUFF_MISSES_BEFORE_RESTART: + log.warning( + "[ConnectionDetector] BuffIcon not found (miss #%d/%d), re-checking in %.0fs...", + self._buff_miss_count, BUFF_MISSES_BEFORE_RESTART, BUFF_MISS_RECHECK_SEC, + ) + self._last_run_time = now - self._interval_sec + BUFF_MISS_RECHECK_SEC # schedule re-check + return + + # BUFF_MISSES_BEFORE_RESTART consecutive misses → assume disconnected, kill and restart + self._buff_miss_count = 0 + log.warning("[ConnectionDetector] BuffIcon not found %d times in a row → disconnection, killing and restarting LastZ", BUFF_MISSES_BEFORE_RESTART) + _kill_process(pid, log) + time.sleep(KILL_WAIT_SEC) + self._last_start_time = time.time() + self._stamp_autostart() + if _start_lastz(self._lastz_exe, log): + time.sleep(WAIT_AFTER_START_SEC) + hwnd, new_pid = _find_window_and_pid(self._window_name) + if hwnd is not None: + wincap.hwnd = hwnd + wincap.refresh_geometry() + self._lastz_pid_ref["pid"] = new_pid + log.info("[ConnectionDetector] LastZ restarted, window reattached, PID=%s", new_pid) diff --git a/kha_lastz_auto/debug/match_count_PasswordSlot_20260227_191905_screenshot.png b/kha_lastz_auto/debug/match_count_PasswordSlot_20260227_191905_screenshot.png new file mode 100644 index 0000000..0a025a7 Binary files /dev/null and b/kha_lastz_auto/debug/match_count_PasswordSlot_20260227_191905_screenshot.png differ diff --git a/kha_lastz_auto/debug/match_count_PasswordSlot_20260227_191905_template.png b/kha_lastz_auto/debug/match_count_PasswordSlot_20260227_191905_template.png new file mode 100644 index 0000000..2461321 Binary files /dev/null and b/kha_lastz_auto/debug/match_count_PasswordSlot_20260227_191905_template.png differ diff --git a/kha_lastz_auto/debug/slot_area_inspect.png b/kha_lastz_auto/debug/slot_area_inspect.png new file mode 100644 index 0000000..d6b2fbb Binary files /dev/null and b/kha_lastz_auto/debug/slot_area_inspect.png differ diff --git a/kha_lastz_auto/debug_exclude_score.py b/kha_lastz_auto/debug_exclude_score.py new file mode 100644 index 0000000..d840717 --- /dev/null +++ b/kha_lastz_auto/debug_exclude_score.py @@ -0,0 +1,141 @@ +""" +Debug script: why does find_multi_with_scores give 0.583 for RadarRedDot +while raw matchTemplate gives 0.9951? + +Usage: + python kha_lastz_auto/debug_exclude_score.py [crop_path] [needle_path] + +Defaults to the problematic saved crop. +""" +import sys +import os +import numpy as np +import cv2 as cv + +sys.path.insert(0, os.path.join(os.path.dirname(__file__))) +from vision import Vision, set_global_scale + +CROP_PATH = os.path.join(os.path.dirname(__file__), + "debug_exclude", "exclude_kept_140319_432_462,618.png") +NEEDLE_PATH = os.path.join(os.path.dirname(__file__), + "buttons_template", "RadarRedDot.png") + +if len(sys.argv) >= 3: + CROP_PATH, NEEDLE_PATH = sys.argv[1], sys.argv[2] +elif len(sys.argv) == 2: + CROP_PATH = sys.argv[1] + +set_global_scale(1.0) + +crop = cv.imread(CROP_PATH) +needle = cv.imread(NEEDLE_PATH, cv.IMREAD_UNCHANGED) +vision = Vision(NEEDLE_PATH) + +if crop is None: + print("[ERROR] Cannot load crop:", CROP_PATH) + sys.exit(1) + +print("=" * 60) +print("crop shape:", crop.shape) +print("needle shape:", needle.shape, "-> after BGRA->BGR:", vision.needle_img.shape) +print() + +# ── 1. Raw matchTemplate (gray) best score ──────────────────── +crop_gray = cv.cvtColor(crop, cv.COLOR_BGR2GRAY) +result_gray = cv.matchTemplate(crop_gray, vision.needle_gray, cv.TM_CCOEFF_NORMED) +_, raw_best, _, raw_loc = cv.minMaxLoc(result_gray) +print("[1] Raw gray matchTemplate") +print(" best score : {:.4f}".format(raw_best)) +print(" best loc : {} (top-left of needle placement)".format(raw_loc)) +center_x = raw_loc[0] + vision.needle_w // 2 +center_y = raw_loc[1] + vision.needle_h // 2 +print(" center : ({}, {})".format(center_x, center_y)) +print() + +# ── 2. How many pixels pass various thresholds? ─────────────── +print("[2] Number of result pixels >= threshold") +for thr in [0.9, 0.7, 0.5, 0.3, 0.1, 0.01, 0.0]: + count = int(np.sum(result_gray >= thr)) + print(" >= {:.2f} : {:>6} pixels".format(thr, count)) +print() + +# ── 3. What groupRectangles does with threshold=0.01 ───────── +print("[3] groupRectangles behaviour with threshold=0.01 (root cause)") +locs_001 = list(zip(*np.where(result_gray >= 0.01)[::-1])) +rects_001 = [] +for loc in locs_001: + r = [int(loc[0]), int(loc[1]), vision.needle_w, vision.needle_h] + rects_001.append(r) + rects_001.append(r) +print(" raw rects fed to groupRectangles : {}".format(len(rects_001) // 2)) +grouped_001, _ = cv.groupRectangles(rects_001, groupThreshold=1, eps=0.5) +print(" output groups : {}".format(len(grouped_001))) +for g in grouped_001: + x, y, w, h = g + cx, cy = x + w // 2, y + h // 2 + r_w = min(w, result_gray.shape[1] - x) + r_h = min(h, result_gray.shape[0] - y) + score = float(np.max(result_gray[y:y+r_h, x:x+r_w])) if r_w > 0 and r_h > 0 else 0.0 + print(" group rect ({:>3},{:>3} {:>2}x{:>2}) -> center=({},{}) max_score_in_rect={:.4f}".format( + x, y, w, h, cx, cy, score)) +print() + +# ── 4. find_multi_with_scores result ───────────────────────── +print("[4] find_multi_with_scores(threshold=0.01, is_color=False)") +pts = vision.find_multi_with_scores(crop, threshold=0.01, is_color=False) +print(" returned:", pts) +print() + +# ── 5. Correct approach: match_score() ─────────────────────── +correct_score = vision.match_score(crop) +print("[5] vision.match_score(crop) = {:.4f} <- this is the correct value".format(correct_score)) +print() + +# ── 6. What groupRectangles does with a SANE threshold ──────── +print("[6] groupRectangles with threshold=0.5 (sane)") +locs_05 = list(zip(*np.where(result_gray >= 0.5)[::-1])) +rects_05 = [] +for loc in locs_05: + r = [int(loc[0]), int(loc[1]), vision.needle_w, vision.needle_h] + rects_05.append(r) + rects_05.append(r) +print(" raw rects fed to groupRectangles : {}".format(len(rects_05) // 2)) +grouped_05, _ = cv.groupRectangles(rects_05, groupThreshold=1, eps=0.5) +print(" output groups : {}".format(len(grouped_05))) +for g in grouped_05: + x, y, w, h = g + cx, cy = x + w // 2, y + h // 2 + r_w = min(w, result_gray.shape[1] - x) + r_h = min(h, result_gray.shape[0] - y) + score = float(np.max(result_gray[y:y+r_h, x:x+r_w])) if r_w > 0 and r_h > 0 else 0.0 + print(" group rect ({:>3},{:>3} {:>2}x{:>2}) -> center=({},{}) max_score_in_rect={:.4f}".format( + x, y, w, h, cx, cy, score)) +print() + +# ── 7. Save annotated debug image ───────────────────────────── +out_path = os.path.join(os.path.dirname(CROP_PATH), + os.path.splitext(os.path.basename(CROP_PATH))[0] + "_debug_annotated.png") +vis = crop.copy() +# Mark raw best match (green) +cv.rectangle(vis, raw_loc, (raw_loc[0]+vision.needle_w, raw_loc[1]+vision.needle_h), (0,255,0), 1) +cv.putText(vis, "{:.2f}".format(raw_best), (raw_loc[0], raw_loc[1]-2), + cv.FONT_HERSHEY_SIMPLEX, 0.35, (0,255,0), 1) +# Mark find_multi_with_scores results (red) +for p in pts: + rx = p[0] - p[2] // 2 + ry = p[1] - p[3] // 2 + cv.rectangle(vis, (rx, ry), (rx+p[2], ry+p[3]), (0,0,255), 1) + cv.putText(vis, "{:.2f}".format(p[4]), (rx, ry-2), + cv.FONT_HERSHEY_SIMPLEX, 0.35, (0,0,255), 1) +cv.imwrite(out_path, vis) +print("[7] Annotated image saved ->", out_path) +print(" GREEN = raw best match (correct) | RED = find_multi_with_scores (wrong)") +print() +print("=" * 60) +print("CONCLUSION") +print(" find_multi_with_scores uses groupRectangles with threshold=0.01.") +print(" With threshold=0.01, almost ALL result pixels pass -> thousands of") +print(" near-identical rects fed to groupRectangles -> they merge into an") +print(" averaged position that doesn't correspond to the true best match.") +print(" The correct fix: use match_score() for the exclude check, which") +print(" directly returns minMaxLoc best score without groupRectangles.") diff --git a/kha_lastz_auto/debug_match_click_Treasure2.png b/kha_lastz_auto/debug_match_click_Treasure2.png new file mode 100644 index 0000000..8319a23 Binary files /dev/null and b/kha_lastz_auto/debug_match_click_Treasure2.png differ diff --git a/kha_lastz_auto/debug_ocr_ocr_log_screen.png b/kha_lastz_auto/debug_ocr_ocr_log_screen.png new file mode 100644 index 0000000..57cfa78 Binary files /dev/null and b/kha_lastz_auto/debug_ocr_ocr_log_screen.png differ diff --git a/kha_lastz_auto/debug_set_level_roi.png b/kha_lastz_auto/debug_set_level_roi.png new file mode 100644 index 0000000..0738967 Binary files /dev/null and b/kha_lastz_auto/debug_set_level_roi.png differ diff --git a/kha_lastz_auto/debug_set_level_roi_raw.png b/kha_lastz_auto/debug_set_level_roi_raw.png new file mode 100644 index 0000000..6b092f5 Binary files /dev/null and b/kha_lastz_auto/debug_set_level_roi_raw.png differ diff --git a/kha_lastz_auto/events/__init__.py b/kha_lastz_auto/events/__init__.py new file mode 100644 index 0000000..19afd05 --- /dev/null +++ b/kha_lastz_auto/events/__init__.py @@ -0,0 +1,2 @@ +# Event handler modules. +# Each file in this package implements one event_type used by bot_engine.py. diff --git a/kha_lastz_auto/events/event_arena_filter.py b/kha_lastz_auto/events/event_arena_filter.py new file mode 100644 index 0000000..29dc2f8 --- /dev/null +++ b/kha_lastz_auto/events/event_arena_filter.py @@ -0,0 +1,253 @@ +""" +event_arena_filter.py +--------------------- +Handler for the ``arena_filter`` event type. + +Flow +---- +1. (Optional) Sleep ``sleep_before_ocr`` seconds so the challenge screen fully renders. +2. Take a fresh screenshot, then OCR *my_power* from an absolute screen-ratio region. +3. OCR each opponent's power from their respective ratio regions. +4. Collect opponents whose power is strictly less than my_power. +5. Click the attack button of the weakest eligible opponent, then advance the step (success). + +Follow-up UI (Combat, deploy, etc.) should be separate YAML steps (e.g. ``match_click``). + +YAML keys +--------- +my_power_region : {x, y, w, h} + Absolute screen ratios for the self-power label. +opponents : list of {x, y, w, h, attack_x, attack_y} + x/y/w/h → ratio region for OCR + attack_x/y → ratio click position for the Attack button +sleep_before_ocr : float + Seconds to wait after the step starts before taking the OCR screenshot. + Allows the challenge screen to finish rendering. Default: 1.5 +debug_save : bool + When true, each OCR crop is saved to ``debug_ocr/arena_filter_