Added Identification Overlay Node

This commit is contained in:
Nicole Rappe 2025-02-26 01:15:25 -07:00
parent c7cbd1ae0d
commit 981d5cb573
6 changed files with 720 additions and 15 deletions

View File

@ -0,0 +1,542 @@
#!/usr/bin/env python3
import sys
import time
import re
import numpy as np
import cv2
import pytesseract
try:
import winsound
HAS_WINSOUND = True
except ImportError:
HAS_WINSOUND = False
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import Qt, QRect, QPoint, QTimer
from PyQt5.QtGui import QPainter, QPen, QColor, QFont
from PIL import Image, ImageGrab, ImageFilter
from rich.console import Console, Group
from rich.table import Table
from rich.progress import Progress, BarColumn, TextColumn
from rich.text import Text
from rich.live import Live
# =============================================================================
# Global Config
# =============================================================================
pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
POLLING_RATE_MS = 500
MAX_DATA_POINTS = 8
# We still use these defaults for Region size.
DEFAULT_WIDTH = 180
DEFAULT_HEIGHT = 130
HANDLE_SIZE = 8
LABEL_HEIGHT = 20
GREEN_HEADER_STYLE = "bold green"
BEEP_INTERVAL_SECONDS = 1.0 # Only beep once every 1 second
# STATUS BAR AUTO-LOCATOR LOGIC (WILL BE BUILT-OUT TO BE MORE ROBUST LATER)
TEMPLATE_PATH = "G:\\Nextcloud\\Projects\\Scripting\\bars_template.png" # Path to your bars template file
MATCH_THRESHOLD = 0.4 # The correlation threshold to consider a "good" match
# =============================================================================
# Helper Functions
# =============================================================================
def beep_hp_warning():
"""
Only beep if enough time has elapsed since the last beep (BEEP_INTERVAL_SECONDS).
"""
current_time = time.time()
if (beep_hp_warning.last_beep_time is None or
(current_time - beep_hp_warning.last_beep_time >= BEEP_INTERVAL_SECONDS)):
beep_hp_warning.last_beep_time = current_time
if HAS_WINSOUND:
# frequency=376 Hz, duration=100 ms
winsound.Beep(376, 100)
else:
# Attempt terminal bell
print('\a', end='')
beep_hp_warning.last_beep_time = None
def locate_bars_opencv(template_path, threshold=MATCH_THRESHOLD):
"""
Attempt to locate the bars via OpenCV template matching:
1) Grab the full screen using PIL.ImageGrab.
2) Convert to NumPy array in BGR format for cv2.
3) Load template from `template_path`.
4) Use cv2.matchTemplate to find the best match location.
5) If max correlation > threshold, return (x, y, w, h).
6) Else return None.
"""
# 1) Capture full screen
screenshot_pil = ImageGrab.grab()
screenshot_np = np.array(screenshot_pil) # shape (H, W, 4) possibly
# Convert RGBA or RGB to BGR
screenshot_bgr = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR)
# 2) Load template from file
template_bgr = cv2.imread(template_path, cv2.IMREAD_COLOR)
if template_bgr is None:
print(f"[WARN] Could not load template file: {template_path}")
return None
# 3) Template matching
result = cv2.matchTemplate(screenshot_bgr, template_bgr, cv2.TM_CCOEFF_NORMED)
# 4) Find best match
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
# template width/height
th, tw, _ = template_bgr.shape
if max_val >= threshold:
# max_loc is top-left corner of the best match
found_x, found_y = max_loc
return (found_x, found_y, tw, th)
else:
return None
def format_duration(seconds):
if seconds is None:
return "???"
seconds = int(seconds)
hours = seconds // 3600
leftover = seconds % 3600
mins = leftover // 60
secs = leftover % 60
if hours > 0:
return f"{hours}h {mins}m {secs}s"
else:
return f"{mins}m {secs}s"
def sanitize_experience_string(raw_text):
text_no_percent = raw_text.replace('%', '')
text_no_spaces = text_no_percent.replace(' ', '')
cleaned = re.sub(r'[^0-9\.]', '', text_no_spaces)
match = re.search(r'\d+(?:\.\d+)?', cleaned)
if not match:
return None
val = float(match.group(0))
if val < 0:
val = 0
elif val > 100:
val = 100
return round(val, 4)
def format_experience_value(value):
if value < 0:
value = 0
elif value > 100:
value = 100
float_4 = round(value, 4)
raw_str = f"{float_4:.4f}"
int_part, dec_part = raw_str.split('.')
if int_part == "100":
pass
elif len(int_part) == 1 and int_part != "0":
int_part = "0" + int_part
elif int_part == "0":
int_part = "00"
return f"{int_part}.{dec_part}"
# -----------------------------------------------------------------------------
# Region Class
# -----------------------------------------------------------------------------
class Region:
"""
Defines a draggable/resizable screen region for OCR capture.
"""
def __init__(self, x, y, label="Region", color=QColor(0,0,255)):
self.x = x
self.y = y
self.w = DEFAULT_WIDTH
self.h = DEFAULT_HEIGHT
self.label = label
self.color = color
self.visible = True
self.data = ""
def rect(self):
return QRect(self.x, self.y, self.w, self.h)
def label_rect(self):
return QRect(self.x, self.y - LABEL_HEIGHT, self.w, LABEL_HEIGHT)
def resize_handles(self):
return [
QRect(self.x - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
QRect(self.x + self.w - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
QRect(self.x - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
QRect(self.x + self.w - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
]
# -----------------------------------------------------------------------------
# OverlayCanvas Class
# -----------------------------------------------------------------------------
class OverlayCanvas(QWidget):
"""
Renders the overlay & handles region dragging/resizing.
"""
def __init__(self, regions, parent=None):
super().__init__(parent)
self.regions = regions
self.edit_mode = True
self.selected_region = None
self.selected_handle = None
self.drag_offset = QPoint()
def paintEvent(self, event):
painter = QPainter(self)
for region in self.regions:
if region.visible:
pen = QPen(region.color)
pen.setWidth(3)
painter.setPen(pen)
painter.drawRect(region.x, region.y, region.w, region.h)
painter.setFont(QFont("Arial", 12, QFont.Bold))
painter.setPen(region.color)
painter.drawText(region.x, region.y - 5, region.label)
if self.edit_mode:
for handle in region.resize_handles():
painter.fillRect(handle, region.color)
def mousePressEvent(self, event):
if not self.edit_mode:
return
if event.button() == Qt.LeftButton:
for region in reversed(self.regions):
for i, handle in enumerate(region.resize_handles()):
if handle.contains(event.pos()):
self.selected_region = region
self.selected_handle = i
return
if region.label_rect().contains(event.pos()):
self.selected_region = region
self.selected_handle = None
self.drag_offset = event.pos() - QPoint(region.x, region.y)
return
if region.rect().contains(event.pos()):
self.selected_region = region
self.selected_handle = None
self.drag_offset = event.pos() - QPoint(region.x, region.y)
return
def mouseMoveEvent(self, event):
if not self.edit_mode or self.selected_region is None:
return
if self.selected_handle is None:
self.selected_region.x = event.x() - self.drag_offset.x()
self.selected_region.y = event.y() - self.drag_offset.y()
else:
sr = self.selected_region
if self.selected_handle == 0: # top-left
sr.w += sr.x - event.x()
sr.h += sr.y - event.y()
sr.x = event.x()
sr.y = event.y()
elif self.selected_handle == 1: # top-right
sr.w = event.x() - sr.x
sr.h += sr.y - event.y()
sr.y = event.y()
elif self.selected_handle == 2: # bottom-left
sr.w += sr.x - event.x()
sr.h = event.y() - sr.y
sr.x = event.x()
elif self.selected_handle == 3: # bottom-right
sr.w = event.x() - sr.x
sr.h = event.y() - sr.y
sr.w = max(sr.w, 10)
sr.h = max(sr.h, 10)
self.update()
def mouseReleaseEvent(self, event):
if not self.edit_mode:
return
if event.button() == Qt.LeftButton:
self.selected_region = None
self.selected_handle = None
# -----------------------------------------------------------------------------
# BorealisOverlay Class
# -----------------------------------------------------------------------------
class BorealisOverlay(QWidget):
"""
Single Region Overlay for Player Stats (HP/MP/FP/EXP) with:
- Automatic location via OpenCV template matching at startup
- OCR scanning
- Low-HP beep
- Rich Live updates in terminal
"""
def __init__(self, live=None):
super().__init__()
screen_geo = QApplication.primaryScreen().geometry()
self.setGeometry(screen_geo)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground, True)
# Try to find the bars automatically
# If found => use that location, else default
initial_x, initial_y = 250, 50
region_w, region_h = DEFAULT_WIDTH, DEFAULT_HEIGHT
match_result = locate_bars_opencv(TEMPLATE_PATH, MATCH_THRESHOLD)
if match_result is not None:
found_x, found_y, w, h = match_result
print(f"Character Status Located at {found_x}, {found_y} with confidence >= {MATCH_THRESHOLD}.")
initial_x, initial_y = found_x, found_y
# Optionally override region size with template size
region_w, region_h = w, h
else:
print("Could not auto-locate the character status page. Set your theme to Masquerade and Interface Scale to 140%, and browser zoom level to 110%. Using default region.")
region = Region(initial_x, initial_y, label="Character Status")
region.w = region_w
region.h = region_h
self.regions = [region]
self.canvas = OverlayCanvas(self.regions, self)
self.canvas.setGeometry(self.rect())
# Tesseract
self.engine = pytesseract
# Keep history of EXP data
self.points = []
self.live = live
# Timer for periodic OCR scanning
self.timer = QTimer(self)
self.timer.timeout.connect(self.collect_ocr_data)
self.timer.start(POLLING_RATE_MS)
def set_live(self, live):
self.live = live
def collect_ocr_data(self):
for region in self.regions:
if region.visible:
screenshot = ImageGrab.grab(
bbox=(region.x, region.y, region.x + region.w, region.y + region.h)
)
processed = self.preprocess_image(screenshot)
text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1')
region.data = text.strip()
if self.live is not None:
renderable = self.build_renderable()
self.live.update(renderable)
def preprocess_image(self, image):
gray = image.convert("L")
scaled = gray.resize((gray.width * 3, gray.height * 3))
thresh = scaled.point(lambda p: p > 200 and 255)
return thresh.filter(ImageFilter.MedianFilter(3))
def parse_all_stats(self, raw_text):
raw_lines = raw_text.splitlines()
lines = [l.strip() for l in raw_lines if l.strip()]
stats_dict = {
"hp": (0,1),
"mp": (0,1),
"fp": (0,1),
"exp": None
}
if len(lines) < 4:
return stats_dict
hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0])
if hp_match:
stats_dict["hp"] = (int(hp_match.group(1)), int(hp_match.group(2)))
mp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[1])
if mp_match:
stats_dict["mp"] = (int(mp_match.group(1)), int(mp_match.group(2)))
fp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[2])
if fp_match:
stats_dict["fp"] = (int(fp_match.group(1)), int(fp_match.group(2)))
exp_val = sanitize_experience_string(lines[3])
stats_dict["exp"] = exp_val
return stats_dict
def update_points(self, new_val):
now = time.time()
if self.points:
_, last_v = self.points[-1]
if abs(new_val - last_v) < 1e-6:
return
if new_val < last_v:
self.points.clear()
self.points.append((now, new_val))
if len(self.points) > MAX_DATA_POINTS:
self.points.pop(0)
def compute_time_to_100(self):
n = len(self.points)
if n < 2:
return None
first_t, first_v = self.points[0]
last_t, last_v = self.points[-1]
diff_v = last_v - first_v
if diff_v <= 0:
return None
steps = n - 1
total_time = last_t - first_t
if total_time <= 0:
return None
avg_change = diff_v / steps
remain = 100.0 - last_v
if remain <= 0:
return None
avg_time = total_time / steps
rate_per_s = avg_change / avg_time if avg_time > 0 else 0
if rate_per_s <= 0:
return None
return int(remain / rate_per_s)
def build_renderable(self):
raw_text = self.regions[0].data
stats = self.parse_all_stats(raw_text)
hp_cur, hp_max = stats["hp"]
mp_cur, mp_max = stats["mp"]
fp_cur, fp_max = stats["fp"]
exp_val = stats["exp"]
# HP beep logic
if hp_max > 0:
hp_ratio = hp_cur / hp_max
if 0 < hp_ratio <= 0.40:
beep_hp_warning()
if exp_val is not None:
self.update_points(exp_val)
current_exp = self.points[-1][1] if self.points else 0.0
# Title
title_text = Text("Project Borealis\n", style="bold white")
subtitle_text = Text("Flyff Information Overlay\n\n", style="dim")
# HP / MP / FP bars
bar_progress = Progress(
"{task.description}",
BarColumn(bar_width=30),
TextColumn(" {task.completed}/{task.total} ({task.percentage:>5.2f}%)"),
transient=False,
auto_refresh=False
)
bar_progress.add_task("[bold red]HP[/bold red]", total=hp_max, completed=hp_cur,
style="red", complete_style="red")
bar_progress.add_task("[bold blue]MP[/bold blue]", total=mp_max, completed=mp_cur,
style="blue", complete_style="blue")
bar_progress.add_task("[bold green]FP[/bold green]", total=fp_max, completed=fp_cur,
style="green", complete_style="green")
bar_progress.refresh()
# Historical EXP table
table = Table(show_header=True, header_style=GREEN_HEADER_STYLE, style=None)
table.add_column("Historical EXP", justify="center", style="green")
table.add_column("Time Since Last Kill", justify="center", style="green")
table.add_column("Average EXP Per Kill", justify="center", style="green")
table.add_column("Average Time Between Kills", justify="center", style="green")
n = len(self.points)
if n == 0:
table.add_row("N/A", "N/A", "N/A", "N/A")
elif n == 1:
_, v0 = self.points[0]
exp_str = f"[green]{format_experience_value(v0)}%[/green]"
table.add_row(exp_str, "N/A", "N/A", "N/A")
else:
for i in range(1, n):
t_cur, v_cur = self.points[i]
t_prev, v_prev = self.points[i - 1]
delta_v = v_cur - v_prev
delta_str = f"{delta_v:+.4f}%"
exp_main = format_experience_value(v_cur)
exp_str = f"[green]{exp_main}%[/green] [dim]({delta_str})[/dim]"
delta_t = t_cur - t_prev
t_since_str = f"{delta_t:.1f}s"
diff_v = v_cur - self.points[0][1]
steps = i
avg_exp_str = f"{diff_v/steps:.4f}%"
total_time = t_cur - self.points[0][0]
avg_kill_time = total_time / steps
avg_time_str = f"{avg_kill_time:.1f}s"
table.add_row(exp_str, t_since_str, avg_exp_str, avg_time_str)
# Predicted Time to Level
secs_left = self.compute_time_to_100()
time_str = format_duration(secs_left)
time_bar = Progress(
TextColumn("[bold white]Predicted Time to Level:[/bold white] "),
BarColumn(bar_width=30, complete_style="magenta"),
TextColumn(" [green]{task.percentage:>5.2f}%[/green] "),
TextColumn(f"[magenta]{time_str}[/magenta] until 100%", justify="right"),
transient=False,
auto_refresh=False
)
time_bar.add_task("", total=100, completed=current_exp)
time_bar.refresh()
return Group(
title_text,
subtitle_text,
bar_progress,
table,
time_bar
)
# -----------------------------------------------------------------------------
# main
# -----------------------------------------------------------------------------
def main():
"""
1) Attempt to locate HP/MP/FP/Exp bars using OpenCV template matching.
2) Position overlay region accordingly if found, else default.
3) Start PyQt, periodically OCR the region, update Rich Live in terminal.
"""
app = QApplication(sys.argv)
window = BorealisOverlay()
window.setWindowTitle("Project Borealis Overlay (HP/MP/FP/EXP)")
window.show()
console = Console()
with Live(console=console, refresh_per_second=4) as live:
window.set_live(live)
exit_code = app.exec_()
sys.exit(exit_code)
if __name__ == "__main__":
main()

View File

@ -25,6 +25,7 @@ regions = {}
app_instance = None
def _ensure_qapplication():
"""
Ensures that QApplication is initialized before creating widgets.
@ -34,12 +35,14 @@ def _ensure_qapplication():
app_instance = QApplication(sys.argv)
threading.Thread(target=app_instance.exec_, daemon=True).start()
def create_ocr_region(region_id, x=250, y=50, w=DEFAULT_WIDTH, h=DEFAULT_HEIGHT):
def create_ocr_region(region_id, x=250, y=50, w=DEFAULT_WIDTH, h=DEFAULT_HEIGHT, color=(255, 255, 0)):
"""
Creates an OCR region with a visible, resizable box on the screen.
The color parameter allows customization (default yellow, blue for overlays).
"""
_ensure_qapplication() # Ensure QApplication is running first
_ensure_qapplication()
collector_mutex.lock()
if region_id in regions:
@ -48,10 +51,11 @@ def create_ocr_region(region_id, x=250, y=50, w=DEFAULT_WIDTH, h=DEFAULT_HEIGHT)
regions[region_id] = {
'bbox': [x, y, w, h],
'raw_text': "",
'widget': OCRRegionWidget(x, y, w, h, region_id)
'widget': OCRRegionWidget(x, y, w, h, region_id, color)
}
collector_mutex.unlock()
def get_raw_text(region_id):
collector_mutex.lock()
if region_id not in regions:
@ -61,10 +65,12 @@ def get_raw_text(region_id):
collector_mutex.unlock()
return text
def start_collector():
t = threading.Thread(target=_update_ocr_loop, daemon=True)
t.start()
def _update_ocr_loop():
while True:
collector_mutex.lock()
@ -79,25 +85,102 @@ def _update_ocr_loop():
x, y, w, h = bbox
screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h))
processed = _preprocess_image(screenshot)
raw_text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1')
raw_text = pytesseract.image_to_string(processed, config='--psm 6 --oem 1')
collector_mutex.lock()
if rid in regions:
regions[rid]['raw_text'] = raw_text
collector_mutex.unlock()
# print(f"OCR Text for {rid}: {raw_text}") # SHOW RAW OCR OUTPUT IN TERMINAL FOR DEBUGGING
time.sleep(0.7)
def _preprocess_image(image):
gray = image.convert("L")
scaled = gray.resize((gray.width * 3, gray.height * 3))
thresh = scaled.point(lambda p: 255 if p > 200 else 0)
return thresh.filter(ImageFilter.MedianFilter(3))
def find_word_positions(region_id, word, offset_x=0, offset_y=0, margin=5):
"""
Finds positions of a specific word within the OCR region.
Applies user-defined offset and margin adjustments.
Returns a list of bounding box coordinates relative to the OCR box.
"""
collector_mutex.lock()
if region_id not in regions:
collector_mutex.unlock()
return []
bbox = regions[region_id]['bbox']
collector_mutex.unlock()
# Extract OCR region position and size
x, y, w, h = bbox
left, top, right, bottom = x, y, x + w, y + h
if right <= left or bottom <= top:
print(f"[ERROR] Invalid OCR region bounds: {bbox}")
return []
try:
image = ImageGrab.grab(bbox=(left, top, right, bottom))
processed = _preprocess_image(image)
# Get original and processed image sizes
orig_width, orig_height = image.size
proc_width, proc_height = processed.size
# Scale factor between processed image and original screenshot
scale_x = orig_width / proc_width
scale_y = orig_height / proc_height
data = pytesseract.image_to_data(processed, config='--psm 6 --oem 1', output_type=pytesseract.Output.DICT)
word_positions = []
for i in range(len(data['text'])):
if re.search(rf"\b{word}\b", data['text'][i], re.IGNORECASE):
# Scale the detected coordinates back to region-relative positions
x_scaled = int(data['left'][i] * scale_x)
y_scaled = int(data['top'][i] * scale_y)
w_scaled = int(data['width'][i] * scale_x)
h_scaled = int(data['height'][i] * scale_y)
# Apply user-configured margin
x_margin = max(0, x_scaled - margin)
y_margin = max(0, y_scaled - margin)
w_margin = w_scaled + (margin * 2)
h_margin = h_scaled + (margin * 2)
# Apply user-configured offset
x_final = x_margin + offset_x
y_final = y_margin + offset_y
word_positions.append((x_final, y_final, w_margin, h_margin))
return word_positions
except Exception as e:
print(f"[ERROR] Failed to capture OCR region: {e}")
return []
def draw_identification_boxes(region_id, positions, color=(0, 0, 255)):
"""
Draws non-interactive rectangles at specified positions within the given OCR region.
"""
collector_mutex.lock()
if region_id in regions and 'widget' in regions[region_id]:
widget = regions[region_id]['widget']
widget.set_draw_positions(positions, color)
collector_mutex.unlock()
class OCRRegionWidget(QWidget):
def __init__(self, x, y, w, h, region_id):
def __init__(self, x, y, w, h, region_id, color):
super().__init__()
self.setGeometry(x, y, w, h)
@ -108,23 +191,41 @@ class OCRRegionWidget(QWidget):
self.drag_offset = None
self.selected_handle = None
self.region_id = region_id
self.box_color = QColor(*color)
self.draw_positions = []
self.show()
def paintEvent(self, event):
painter = QPainter(self)
pen = QPen(QColor(255, 255, 0)) # COLOR OF THE BOX ITSELF
pen.setWidth(5) # WIDTH OF THE BOX BORDER
pen = QPen(self.box_color)
pen.setWidth(5)
painter.setPen(pen)
# Draw main rectangle
painter.drawRect(0, 0, self.width(), self.height())
# Draw detected word overlays
pen.setWidth(2)
pen.setColor(QColor(0, 0, 255))
painter.setPen(pen)
for x, y, w, h in self.draw_positions:
painter.drawRect(x, y, w, h)
# Draw resize handles
painter.setBrush(QColor(255, 255, 0)) # COLOR OF THE RESIZE HANDLES
painter.setBrush(self.box_color)
for handle in self._resize_handles():
painter.drawRect(handle)
def set_draw_positions(self, positions, color):
"""
Update the positions where identification boxes should be drawn.
"""
self.draw_positions = positions
self.box_color = QColor(*color)
self.update()
def _resize_handles(self):
w, h = self.width(), self.height()
return [
@ -174,7 +275,3 @@ class OCRRegionWidget(QWidget):
if self.region_id in regions:
regions[self.region_id]['bbox'] = [new_x, new_y, self.width(), self.height()]
collector_mutex.unlock()
def mouseReleaseEvent(self, event):
self.selected_handle = None
self.drag_offset = None

View File

@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""
Identification Overlay Node:
- Creates an OCR region in data_collector with a blue overlay.
- Detects instances of a specified word and draws adjustable overlays.
- Users can configure offset and margin dynamically.
"""
import re
from OdenGraphQt import BaseNode
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtCore import QTimer
from Modules import data_manager, data_collector
class IdentificationOverlayNode(BaseNode):
__identifier__ = "bunny-lab.io.identification_overlay_node"
NODE_NAME = "Identification Overlay"
def __init__(self):
super(IdentificationOverlayNode, self).__init__()
# User-configurable options
self.add_text_input("search_term", "Search Term", text="Aibatt")
self.add_text_input("offset_value", "Offset Value (X,Y)", text="0,0") # New input
self.add_text_input("margin", "Margin", text="5") # New input
self.region_id = "identification_overlay"
data_collector.create_ocr_region(self.region_id, x=250, y=50, w=300, h=200, color=(0, 0, 255))
data_collector.start_collector()
self.set_name("Identification Overlay")
# Timer for updating overlays
self.timer = QTimer()
self.timer.timeout.connect(self.update_overlay)
self.timer.start(500) # Update every 500ms
def update_overlay(self):
"""
Updates the overlay with detected word positions.
"""
search_term = self.get_property("search_term")
offset_text = self.get_property("offset_value")
margin_text = self.get_property("margin")
# Parse user-defined offset
try:
offset_x, offset_y = map(int, offset_text.split(","))
except ValueError:
offset_x, offset_y = 0, 0 # Default to no offset if invalid input
# Parse user-defined margin
try:
margin = int(margin_text)
except ValueError:
margin = 5 # Default margin if invalid input
if not search_term:
return
# Get detected word positions
detected_positions = data_collector.find_word_positions(self.region_id, search_term, offset_x, offset_y, margin)
# Draw detected word boxes
data_collector.draw_identification_boxes(self.region_id, detected_positions, color=(0, 0, 255))