Compare commits

...

4 Commits

Author SHA1 Message Date
Doc
4c01b72b54 Improved the seeking in the grid calibration
- Make the seeking of the upper left corner more intelligent/responsive rather than hard coding "left 15 times up 15 times"
- Commented out more experimentation code
2026-01-13 10:08:15 -05:00
Doc
4e4d3fc854 Fixed variable name in map size calculation
- accidentlally used cal_right_border instead of cal_bottom_border. Always pay attention to warnings of unused variables
- commented out unused code
2026-01-13 09:57:09 -05:00
Doc
3ab553fb6d Fixes and a few additional helper functions
- corrected the return type of a few functions to int
- Add DFWINDOW.TOOLS.getImageDiff to get how diff 2 images are
- Added complimentary lastNotBlack(X/Y)
- Increase blindly chosen preset "bottom_to_ignore". Wasnt enough when full screen.
- Fixed the sign/polarity of step sizes
- First attempt at calculating the size of the map. Probably wrong. Will replace when writing the map maker
- removed cv2.Stitcher stuff. Not what I needed
- Added some test_ functions for experimenting. Ignore.
2026-01-13 09:52:09 -05:00
Doc
941eae5d57 test1 experiments. some optimizations
- added self.test1 for experimenting cleaner
- began steps to calculate map size
- optimize several functions by moving from mean to max
2026-01-13 04:07:19 -05:00
3 changed files with 169 additions and 52 deletions

1
.gitignore vendored
View File

@@ -231,3 +231,4 @@ Thumbs.db
# experiment garbage # experiment garbage
*.png *.png
src/calib_info.json

View File

@@ -1,3 +1,4 @@
import json
import time import time
from pathlib import Path from pathlib import Path
@@ -8,6 +9,8 @@ from loguru import logger
from .waytools import capActiveWindow, focusWindow, moveMouse from .waytools import capActiveWindow, focusWindow, moveMouse
from .waytools import sendKey as _sendKey from .waytools import sendKey as _sendKey
# TODO: Consider type hinting images from cv2.typing import MatLike
class DFWINDOW: class DFWINDOW:
class TOOLS: class TOOLS:
@@ -29,24 +32,24 @@ class DFWINDOW:
) -> tuple[int, int]: ) -> tuple[int, int]:
# Check the first (num_rows) rows at the top of the image, # Check the first (num_rows) rows at the top of the image,
# ignoring (ignore_cols) number of pixels at each end of teh line. # ignoring (ignore_cols) number of pixels at each end of teh line.
test_mean = np.mean( test_max = np.max(
cv2.cvtColor(image_in[0:num_rows, ignore_cols:-ignore_cols], cv2.COLOR_BGR2GRAY), cv2.cvtColor(image_in[0:num_rows, ignore_cols:-ignore_cols], cv2.COLOR_BGR2GRAY),
axis=1, # get the mean along the x-axis axis=1, # get the mean along the x-axis
) )
# TODO: handle when 0 results return # TODO: handle when 0 results return
# Test the mean darkness, get the first row darker than 4 # Test the mean darkness, get the first row darker than 4
content_y = np.where(test_mean < mean_threshold)[0][0] content_y = np.where(test_max < mean_threshold)[0][0]
_ignore_rows = max(ignore_rows, content_y + 1) _ignore_rows = max(ignore_rows, content_y + 1)
test_mean = np.mean( test_max = np.max(
cv2.cvtColor(image_in[_ignore_rows:-_ignore_rows, 0:num_cols], cv2.COLOR_BGR2GRAY), cv2.cvtColor(image_in[_ignore_rows:-_ignore_rows, 0:num_cols], cv2.COLOR_BGR2GRAY),
axis=0, # get the mean along the y-axis axis=0, # get the mean along the y-axis
) )
content_x = np.where(test_mean < mean_threshold)[0][0] content_x = np.where(test_max < mean_threshold)[0][0]
logger.debug(f"Content origin ({content_x}, {content_y})") logger.debug(f"Content origin ({content_x}, {content_y})")
return (content_x, content_y) return (int(content_x), int(content_y))
@staticmethod @staticmethod
def isRightBorder(img, num_columns=20, border_threshold: int = 10) -> bool: def isRightBorder(img, num_columns=20, border_threshold: int = 10) -> bool:
@@ -58,6 +61,20 @@ class DFWINDOW:
# Are all pixels in the strip "black" # Are all pixels in the strip "black"
return not np.any(thresh) return not np.any(thresh)
@staticmethod
def getImageDiff(image1, image2, conversion=cv2.COLOR_BGR2GRAY) -> float:
# Diff size, very dif img
if image1.shape != image2.shape:
return float("inf")
grey1 = cv2.cvtColor(image1, conversion)
grey2 = cv2.cvtColor(image2, conversion)
# Apparently this is the Mean Squared Error (MES). Thanks Gemini (LLM)
err = np.sum((grey1.astype("float") - grey2.astype("float")) ** 2)
err /= float(grey1.shape[0] * grey1.shape[0])
return float(err)
@staticmethod @staticmethod
def isLeftBorder(img, num_columns=20, border_threshold: int = 10) -> bool: def isLeftBorder(img, num_columns=20, border_threshold: int = 10) -> bool:
# grab a greyscale strip to look at # grab a greyscale strip to look at
@@ -89,20 +106,30 @@ class DFWINDOW:
return not np.any(thresh) return not np.any(thresh)
@staticmethod @staticmethod
def firstNotBlackX(img): def firstNotBlackX(img) -> int:
first_x = np.where(np.mean(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), axis=0) > 15)[0][0] first_x = np.where(np.max(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), axis=0) > 5)[0][0]
return first_x return int(first_x)
@staticmethod @staticmethod
def firstNotBlackY(img): def lastNotBlackX(img) -> int:
first_y = np.where(np.mean(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), axis=1) > 15)[0][0] first_x = np.where(np.max(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), axis=0) > 5)[0][-1]
return first_y return int(first_x)
bottom_to_ignore = 120 @staticmethod
sleep_after_mouse = 0.2 def firstNotBlackY(img) -> int:
sleep_after_key = 0.08 first_y = np.where(np.max(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), axis=1) > 5)[0][0]
sleep_after_focus = 0.3 return int(first_y)
sleep_after_panning = 0.3
@staticmethod
def lastNotBlackY(img) -> int:
first_y = np.where(np.max(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), axis=1) > 5)[0][-1]
return int(first_y)
bottom_to_ignore = 160
sleep_after_mouse = 0.2 # 2
sleep_after_key = 0.08 # 08
sleep_after_focus = 0.2 # 3
sleep_after_panning = 0.2 # 3
query_for_window = "dwarfort" query_for_window = "dwarfort"
def __init__(self) -> None: def __init__(self) -> None:
@@ -220,12 +247,26 @@ class DFWINDOW:
time.sleep(self.sleep_after_mouse) time.sleep(self.sleep_after_mouse)
self.focusWindow() self.focusWindow()
time.sleep(self.sleep_after_focus) time.sleep(self.sleep_after_focus)
self.sendKeys("w", 30)
self.sendKeys("a", 30) # Improved seeking upper left
self.sendKeys("w", 8)
img = self.capContent()
while not self.TOOLS.isTopBorder(img):
self.sendKeys("w", 4)
img = self.capContent()
self.sendKeys("w", 4)
self.sendKeys("a", 8)
img = self.capContent()
while not self.TOOLS.isLeftBorder(img):
self.sendKeys("a", 4)
img = self.capContent()
self.sendKeys("a", 4)
img = self.capWindow() img = self.capWindow()
self._content_left, self._content_top = self.TOOLS.find_content_origin(img) self._content_left, self._content_top = self.TOOLS.find_content_origin(img)
self._content_right = img.shape[1] - self._content_left self._content_right = int(img.shape[1] - self._content_left)
self._content_bottom = img.shape[0] - self.bottom_to_ignore self._content_bottom = int(img.shape[0] - self.bottom_to_ignore)
img = img[self._content_top : self._content_bottom, self._content_left : self._content_right] # pyright: ignore[reportOptionalSubscript] img = img[self._content_top : self._content_bottom, self._content_left : self._content_right] # pyright: ignore[reportOptionalSubscript]
logger.debug(f"Content width {self.contentWidth}. Content height {self.contentHeight}.") logger.debug(f"Content width {self.contentWidth}. Content height {self.contentHeight}.")
@@ -239,8 +280,8 @@ class DFWINDOW:
img = self.capContent() img = self.capContent()
mx2 = self.TOOLS.firstNotBlackX(img) mx2 = self.TOOLS.firstNotBlackX(img)
my2 = self.TOOLS.firstNotBlackY(img) my2 = self.TOOLS.firstNotBlackY(img)
self._step_size_x = mx2 - mx1 self._step_size_x = mx1 - mx2
self._step_size_y = my2 - my1 self._step_size_y = my1 - my2
logger.info(f"Step sizes calculated: x={self._step_size_x} and y={self._step_size_y}") logger.info(f"Step sizes calculated: x={self._step_size_x} and y={self._step_size_y}")
self.sendKeys("w") self.sendKeys("w")
self.sendKeys("a") self.sendKeys("a")
@@ -303,13 +344,6 @@ class DFWINDOW:
self._gridx = steps_right self._gridx = steps_right
self._gridy = steps_down self._gridy = steps_down
# TODO: Use seek tests to calculate mapsize in pixels
# at (0,0) save left_edge_offset and top_edge_offset
# at (max,max) save right_edge_offset and bottom_edge_offset
# width = (contentWidth - l_e_o) + (gridyx_max * _step_size_x) - abs(r_e_o)
# | <==|====|====|==> |
# (max*size) is too far, so we subract the ofset/border from the right map edge
# Test going to 0,0 # Test going to 0,0
self.setGridPos(0, 0) self.setGridPos(0, 0)
time.sleep(self.sleep_after_panning) time.sleep(self.sleep_after_panning)
@@ -318,6 +352,9 @@ class DFWINDOW:
logger.debug("Calibration error. Not at requested upper left of map") logger.debug("Calibration error. Not at requested upper left of map")
raise Exception("Calibration error. Not at requested upper left of map") raise Exception("Calibration error. Not at requested upper left of map")
cal_left_border = self.TOOLS.firstNotBlackX(img)
cal_top_border = self.TOOLS.firstNotBlackY(img)
# Test going to (max,max) # Test going to (max,max)
self.setGridPos(self.maxGridX, self.maxGridY) self.setGridPos(self.maxGridX, self.maxGridY)
time.sleep(self.sleep_after_panning) time.sleep(self.sleep_after_panning)
@@ -326,31 +363,112 @@ class DFWINDOW:
logger.debug("Calibration error. Not at requested lower right of map") logger.debug("Calibration error. Not at requested lower right of map")
raise Exception("Calibration error. Not at requested lower right of map") raise Exception("Calibration error. Not at requested lower right of map")
cal_right_border = self.TOOLS.lastNotBlackX(img)
cal_bottom_border = self.TOOLS.lastNotBlackY(img)
# TODO: Use seek tests to calculate mapsize in pixels
# at (0,0) save left_edge_offset and top_edge_offset
# at (max,max) save right_edge_offset and bottom_edge_offset
# width = (contentWidth - l_e_o) + (gridyx_max * _step_size_x) - abs(r_e_o)
# | <==|====|====|==> |
# (max*size) is too far, so we subract the ofset/border from the right map edge
self._map_width = (
(img.shape[1] - cal_left_border) # Grid x = 0
+ ((self._gridx_max - 1) * self._step_size_x) # All the middle
+ cal_right_border # grid x = max
)
self._map_height = (
(img.shape[0] - cal_top_border) # Grid x = 0
+ ((self._gridy_max - 1) * self._step_size_y) # All the middle
+ cal_bottom_border # grid x = max
)
self.setGridPos(0, 0)
logger.debug(f"Map dimensions calculated as {self._map_width} x {self._map_height}")
logger.info( logger.info(
f"Grid calibration complete. Grid steps ({self._gridy_max + 1},{self._gridy_max + 1}), step sizes({self._step_size_x},{self._step_size_y})" f"Grid calibration complete. Grid steps ({self._gridy_max + 1},{self._gridy_max + 1}), step sizes({self._step_size_x},{self._step_size_y})"
) )
def test1(self):
# rawimg = cv2.imread("./test_img.png")
# rawimg = cv2.imread("grid_base_3.png")
# img = rawimg[100 : -self.bottom_to_ignore - 70, 65:-65]
# tlb = self.TOOLS.firstNotBlackX(img)
# ttb = self.TOOLS.firstNotBlackY(img)
# tt_setup = (
# r"gc.enable() ; import cv2 ; import numpy as np ; timg = cv2.imread('./test_img.png', cv2.IMREAD_UNCHANGED)"
# )
# tt1 = timeit.Timer(
# "np.where(np.mean(cv2.cvtColor(timg, cv2.COLOR_BGR2GRAY), axis=0) > 15)[0][0]",
# setup=tt_setup,
# )
# tt2 = timeit.Timer(
# "np.where(np.max(cv2.cvtColor(timg, cv2.COLOR_BGR2GRAY), axis=0) > 25)[0][0]",
# setup=tt_setup,
# )
# tt3 = timeit.Timer(
# "np.where(np.max(cv2.cvtColor(timg, cv2.COLOR_BGRA2GRAY), axis=0) > 25)[0][0]",
# setup=tt_setup,
# )
# num_tests = 80
# r1 = tt1.timeit(number=num_tests)
# r2 = tt2.timeit(number=num_tests)
# r3 = tt3.timeit(number=num_tests)
logger.debug("Pause here for testing")
def test_saveGrids(self):
savedir = Path()
savefile_base = "cached_grid"
savefile_ext = "png"
for x in range(0, self._gridx_max + 1):
for y in range(0, self._gridy_max):
self.setGridPos(x, y)
time.sleep(self.sleep_after_panning)
img = self.capContent()
savefilename = savedir.joinpath(f"{savefile_base}_{x}_{y}.{savefile_ext}")
cv2.imwrite(str(savefilename.resolve()), img)
calib_info = {
"gridx": int(self._gridx),
"gridy": int(self._gridy),
"gridx_max": int(self._gridx_max),
"gridy_max": int(self._gridy_max),
"step_size_x": int(self._step_size_x),
"step_size_y": int(self._step_size_y),
"content_top": int(self._content_top),
"content_bottom": int(self._content_bottom),
"content_left": int(self._content_left),
"content_right": int(self._content_right),
"map_height": int(self._map_height),
"map_width": int(self._map_width),
}
with open("./calib_info.json", "w") as fh:
json.dump(calib_info, fh)
def test_loadCalib(self):
with open("./calib_info.json") as fh:
calib_info = json.load(fh)
self._gridx = calib_info["gridx"]
self._gridy = calib_info["gridy"]
self._gridx_max = calib_info["gridx_max"]
self._gridy_max = calib_info["gridy_max"]
self._step_size_x = calib_info["step_size_x"]
self._step_size_y = calib_info["step_size_y"]
self._content_top = calib_info["content_top"]
self._content_bottom = calib_info["content_bottom"]
self._content_left = calib_info["content_left"]
self._content_right = calib_info["content_right"]
self._map_height = calib_info["map_height"]
self._map_width = calib_info["map_width"]
def getPanoramaMap(self): def getPanoramaMap(self):
self.calibrateGrid() self.calibrateGrid()
# Test getting pieces and stitching
stitcher = cv2.Stitcher.create(cv2.STITCHER_SCANS)
stitcher.setPanoConfidenceThresh(0.1) # Dont be confident
imgs_in_row = []
# Get a row
self.setGridPos(0, 0)
time.sleep(self.sleep_after_panning)
for x in range(0, self.maxGridX + 1, 3):
self.setGridPos(x, self.curGridY)
time.sleep(self.sleep_after_panning)
img = self.capContent()
if img.shape[2] == 4:
img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
imgs_in_row.append(img)
status, strip = stitcher.stitch(imgs_in_row)
logger.debug(f"{len(imgs_in_row)} images. {status=} {strip=} {status == cv2.Stitcher_OK}")
return None return None

View File

@@ -2,8 +2,6 @@ import json
import subprocess import subprocess
import pydantic as pyd import pydantic as pyd
import cv2
import numpy as np
from .dfwindow import DFWINDOW from .dfwindow import DFWINDOW
from .mylogging import logger, setup_logging from .mylogging import logger, setup_logging