Compare commits

...

8 Commits

Author SHA1 Message Date
Doc
71758c42cc Progress towards stitching a map
- Add gridHeight and gridWidth properties
- Fix the ability of overriding keypress delays
- Fixed/edited debug messages
- Still not sure about map size estimation formula
- Added addToCanvas for created a stitched map
- Create the veritcal part of the loop for stitching together maps
2026-01-14 00:59:32 -05:00
Doc
2c95aa26bb Fixed error in "faster seeking"
- I made a major oversight when speeding up the grid calibration. I forgot some variables were not set until AFTER the calibration. Fixed it so it is still faster/adaptable, but doesnt try to use values not yet set.
2026-01-13 23:04:54 -05:00
Doc
f2c1d04ed4 Add stepSizeX and stepSizeY properties
- Added properties stepSizeX stepSizeY to DFWINDOW
2026-01-13 23:02:18 -05:00
Doc
98b2e3352b Revert pixel search back to slower mean
- *NotBlack* functions reverted to slower np.mean, to fix edge cases with np.max
2026-01-13 22:59:30 -05:00
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 307 additions and 55 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,3 +1,5 @@
import json
import math
import time
from pathlib import Path
@@ -8,6 +10,8 @@ from loguru import logger
from .waytools import capActiveWindow, focusWindow, moveMouse
from .waytools import sendKey as _sendKey
# TODO: Consider type hinting images from cv2.typing import MatLike
class DFWINDOW:
class TOOLS:
@@ -29,24 +33,24 @@ class DFWINDOW:
) -> tuple[int, int]:
# Check the first (num_rows) rows at the top of the image,
# 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),
axis=1, # get the mean along the x-axis
)
# TODO: handle when 0 results return
# 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)
test_mean = np.mean(
test_max = np.max(
cv2.cvtColor(image_in[_ignore_rows:-_ignore_rows, 0:num_cols], cv2.COLOR_BGR2GRAY),
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})")
return (content_x, content_y)
return (int(content_x), int(content_y))
@staticmethod
def isRightBorder(img, num_columns=20, border_threshold: int = 10) -> bool:
@@ -58,6 +62,20 @@ class DFWINDOW:
# Are all pixels in the strip "black"
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
def isLeftBorder(img, num_columns=20, border_threshold: int = 10) -> bool:
# grab a greyscale strip to look at
@@ -89,20 +107,30 @@ class DFWINDOW:
return not np.any(thresh)
@staticmethod
def firstNotBlackX(img):
first_x = np.where(np.mean(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), axis=0) > 15)[0][0]
return first_x
def firstNotBlackX(img) -> int:
first_x = np.where(np.mean(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), axis=0) > 10)[0][0]
return int(first_x)
@staticmethod
def firstNotBlackY(img):
first_y = np.where(np.mean(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), axis=1) > 15)[0][0]
return first_y
def lastNotBlackX(img) -> int:
first_x = np.where(np.mean(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), axis=0) > 10)[0][-1]
return int(first_x)
bottom_to_ignore = 120
sleep_after_mouse = 0.2
sleep_after_key = 0.08
sleep_after_focus = 0.3
sleep_after_panning = 0.3
@staticmethod
def firstNotBlackY(img) -> int:
first_y = np.where(np.mean(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), axis=1) > 10)[0][0]
return int(first_y)
@staticmethod
def lastNotBlackY(img) -> int:
first_y = np.where(np.mean(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), axis=1) > 10)[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"
def __init__(self) -> None:
@@ -137,6 +165,22 @@ class DFWINDOW:
def maxGridY(self) -> int:
return self._gridy_max
@property
def gridHeight(self) -> int:
return int(self._gridy_max + 1)
@property
def gridWidth(self) -> int:
return int(self._gridx_max + 1)
@property
def stepSizeX(self) -> int:
return int(self._step_size_x)
@property
def stepSizeY(self) -> int:
return int(self._step_size_y)
@property
def contentWidth(self) -> int:
return int(self._content_right - self._content_left)
@@ -176,16 +220,17 @@ class DFWINDOW:
thekey: str | int,
count: int = 1,
modifier: str | int | list[str | int] | None = None,
cycle_delay: float = 0.1,
cycle_delay: float = 9999,
sub_cycle_delay: float = 0.05,
custom_lookup: dict[str, int] | None = None,
):
_cycle_delay = cycle_delay if cycle_delay != 9999 else self.sleep_after_key
self.focusWindow()
_sendKey(
thekey=thekey,
count=count,
modifier=modifier,
cycle_delay=self.sleep_after_key,
cycle_delay=_cycle_delay,
sub_cycle_delay=sub_cycle_delay,
custom_lookup=custom_lookup,
)
@@ -220,13 +265,30 @@ class DFWINDOW:
time.sleep(self.sleep_after_mouse)
self.focusWindow()
time.sleep(self.sleep_after_focus)
self.sendKeys("w", 30)
self.sendKeys("a", 30)
img = self.capWindow()
# Improved seeking upper left
self.sendKeys("w", 2)
self.sendKeys("a", 2)
old_img = img
img = self.capWindow()
while self.TOOLS.getImageDiff(old_img, img) > 3:
self.sendKeys("w", 4)
self.sendKeys("a", 4)
old_img = img
img = self.capWindow()
self.sendKeys("w", 4)
self.sendKeys("a", 4)
img = self.capWindow()
self._content_left, self._content_top = self.TOOLS.find_content_origin(img)
self._content_right = img.shape[1] - self._content_left
self._content_bottom = img.shape[0] - self.bottom_to_ignore
self._content_right = int(img.shape[1] - self._content_left)
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]
logger.debug(f"Content width {self.contentWidth}. Content height {self.contentHeight}.")
# Try to measure steps
@@ -239,8 +301,8 @@ class DFWINDOW:
img = self.capContent()
mx2 = self.TOOLS.firstNotBlackX(img)
my2 = self.TOOLS.firstNotBlackY(img)
self._step_size_x = mx2 - mx1
self._step_size_y = my2 - my1
self._step_size_x = mx1 - mx2
self._step_size_y = my1 - my2
logger.info(f"Step sizes calculated: x={self._step_size_x} and y={self._step_size_y}")
self.sendKeys("w")
self.sendKeys("a")
@@ -272,7 +334,7 @@ class DFWINDOW:
# We now know how many steps the map is vertically
steps_vertical = steps_down
logger.debug(f"Map is about {steps_vertical} steps vertical. Current step is {steps_down}")
logger.debug(f"Map is about {steps_vertical+1} steps vertical. Current index is {steps_down}")
# go right until the left map edge disappears
while self.TOOLS.isLeftBorder(img):
@@ -296,20 +358,13 @@ class DFWINDOW:
# And those are the horrizontal steps
steps_horrizontal = steps_right
logger.debug(f"Map is about {steps_horrizontal} steps horrizontal. Current step is {steps_right}")
logger.debug(f"Map is about {steps_horrizontal+1} steps horrizontal. Current index is {steps_right}")
self._gridx_max = steps_horrizontal
self._gridy_max = steps_vertical
self._gridx = steps_right
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
self.setGridPos(0, 0)
time.sleep(self.sleep_after_panning)
@@ -318,6 +373,9 @@ class DFWINDOW:
logger.debug("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)
self.setGridPos(self.maxGridX, self.maxGridY)
time.sleep(self.sleep_after_panning)
@@ -326,31 +384,225 @@ class DFWINDOW:
logger.debug("Calibration error. Not at requested lower right of map")
raise Exception("Calibration error. Not at requested lower right of map")
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})"
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 = (
(self.contentWidth - cal_left_border) # Grid x = 0
+ ((self._gridx_max - 1) * self._step_size_x) # All the middle
+ cal_right_border # grid x = max
)
logger.trace(f"|{self.contentWidth} - {cal_left_border}|({self._gridx_max} - 1) * {self._step_size_x}|{cal_right_border}|")
logger.trace(f"{self._map_width} = |{self.contentWidth - cal_left_border}|{(self._gridx_max - 1) * self._step_size_x}|{cal_right_border}|")
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.debug(
f"Grid calibration complete. Grid steps ({self._gridx_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 addToCanvas(self, tile, x: int, y: int) -> tuple[int, int]:
# calculate safe (in bounds) abs pos of far end
safe_farx = min(x + tile.shape[1], self.map_canvas.shape[1])
safe_fary = min(y + tile.shape[0], self.map_canvas.shape[0])
safe_width = safe_farx - x
safe_height = safe_fary - y
self.map_canvas[y:safe_fary, x:safe_farx] = tile[: (safe_fary - y), : (safe_farx - x)]
logger.trace(f"Added {safe_width}x{safe_height} of tile ({tile.shape[1]}x{tile.shape[0]}) at {x},{y} ")
return (int(safe_width), int(safe_height))
def getPanoramaMap(self):
self.calibrateGrid()
# Test getting pieces and stitching
stitcher = cv2.Stitcher.create(cv2.STITCHER_SCANS)
stitcher.setPanoConfidenceThresh(0.1) # Dont be confident
# Create the big_map canvas
canvas_width = self.contentWidth + (self.stepSizeX * (self.maxGridX + 1 + 1))
canvas_height = self.contentHeight + (self.stepSizeY * (self.maxGridY + 1 + 1))
imgs_in_row = []
self.map_canvas = np.zeros((canvas_height, canvas_width, 4), dtype=np.uint8)
# Get a row
# We want to cap from the content area, minus and black borders.
# starting at canvas_pos of 0,0 Add cap to the canvas
# Then we pan down almost enough to push everything up off the screen
# Then we cap the new stuff, row starting at max(firstNotBlackY, contentHeight - (amount we paned down))
# at canvas_pos add cap to canvas
# increase canvas_pos.y by that new amount
# if we already have a bottom bar, or we are at last grid, break out, otherwise loop
if 1 == 1:
# The initial setup
new_x = self.contentWidth
new_y = self.contentHeight
canvas_pos = [0, 0]
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}")
# Never do more than this many loops
sanity_steps_left = self.maxGridY + 1
while sanity_steps_left > 0:
# Capture a tile
img = self.capContent()
cap_start_x = max(self.TOOLS.firstNotBlackX(img), self.contentWidth - new_x)
cap_start_y = max(self.TOOLS.firstNotBlackY(img), self.contentHeight - new_y)
# use min with other restriction if needed in the future min(lastNotBlack,Other_limit)
cap_end_x = self.TOOLS.lastNotBlackX(img)
cap_end_y = self.TOOLS.lastNotBlackY(img)
pixels_added = self.addToCanvas(
img[cap_start_y : cap_end_y + 1, cap_start_x : cap_end_x + 1], canvas_pos[0], canvas_pos[1]
)
canvas_pos[1] += pixels_added[1]
# Reasons to finish this column:
# - pixels_added[1] < cap_height
# - (with cur logic) cap_height < self.contentHeight
# - self.curGridY >= self.maxGridY
logger.trace(f"{cap_start_y=} {cap_end_y=} {self.contentHeight=} {pixels_added=} {canvas_pos=}")
logger.trace(f"{self.curGridPos=} {self.map_canvas.shape=}")
sanity_steps_left -= 1 # Prevent runaway loops
if not (
(cap_end_y + 1 < self.contentHeight)
or (pixels_added[1] < ((cap_end_y + 1) - cap_start_y))
or (self.curGridY >= self.maxGridY)
or (canvas_pos[1] >= self.map_canvas.shape[0])
):
# pan down for more map, but watch limits
steps_to_pan_down = min(self.maxGridY - self.curGridY, math.floor(self.contentHeight / self.stepSizeY))
self.setGridPos(0, self.curGridY + steps_to_pan_down)
new_y = steps_to_pan_down * self.stepSizeY
else:
break
if sanity_steps_left < 1:
logger.debug(f"Our loop in the Y axis ran over. {sanity_steps_left=}")
if self.map_canvas is not None:
cv2.imwrite("./test_canvas.png", self.map_canvas)
# if 1 == 0:
# self.setGridPos(0, 0)
# canvas_x = 0
# canvas_y = 0
# img = self.capContent()
# startx = self.TOOLS.firstNotBlackX(img)
# starty = self.TOOLS.firstNotBlackY(img)
# img_ul = img[starty:, startx:]
# # cv2.rectangle(img, (startx, starty), (self.contentWidth, self.contentHeight), (255, 255, 255, 255), 3)
# logger.debug(f"img_ul is {img_ul.shape[1]} x {img_ul.shape[0]}")
# last_add = self.addToCanvas(img_ul, 0, 0)
# steps_to_pan_down = math.floor(self.contentHeight / self.stepSizeY)
# logger.debug(f"{startx=} {starty=} {steps_to_pan_down=}")
# self.setGridPos(0, steps_to_pan_down)
# time.sleep(self.sleep_after_panning)
# img = self.capContent()
# new_starty = self.contentHeight - (steps_to_pan_down * self.stepSizeY)
# img_next = img[new_starty:, startx:]
# logger.debug(f"img_next is {img_next.shape[1]} x {img_next.shape[0]}")
# # cv2.rectangle(img, (startx, new_starty), (self.contentWidth, self.contentHeight), (255, 0, 0), 3)
# self.addToCanvas(img_next, 0, last_add[1])
# cv2.imwrite("./test_canvas.png", self.map_canvas)
# a = 1
# logger.debug(f"{new_starty=}")
logger.debug("place to break")
return None

View File

@@ -2,8 +2,6 @@ import json
import subprocess
import pydantic as pyd
import cv2
import numpy as np
from .dfwindow import DFWINDOW
from .mylogging import logger, setup_logging
@@ -561,6 +559,7 @@ def test5():
if __name__ == "__main__":
setup_logging(level="DEBUG", enqueue=False, console_show_time=False, console_tracebacks=True)
# level 5 is TRACE
setup_logging(level=5, enqueue=False, console_show_time=False, console_tracebacks=True)
test5()