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_