Fixed On-Screen OCR region Overlay

This commit is contained in:
Nicole Rappe 2025-02-16 21:07:26 -07:00
parent e30ba4ec4f
commit 6888b55612
5 changed files with 108 additions and 131 deletions

View File

@ -3,12 +3,13 @@
import threading
import time
import re
import sys
import numpy as np
import cv2
import pytesseract
from PIL import Image, ImageGrab, ImageFilter
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import QRect, QPoint, Qt, QMutex, QTimer
from PyQt5.QtGui import QPainter, QPen, QColor, QFont
@ -21,19 +22,37 @@ LABEL_HEIGHT = 20
collector_mutex = QMutex()
regions = {}
overlay_window = None
app_instance = None
def _ensure_qapplication():
"""
Ensures that QApplication is initialized before creating widgets.
"""
global app_instance
if QApplication.instance() is None:
print("Starting QApplication in a separate thread.")
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):
"""
Creates an OCR region with a visible, resizable box on the screen.
"""
print(f"Creating OCR Region: {region_id} at ({x}, {y}, {w}, {h})")
_ensure_qapplication() # Ensure QApplication is running first
collector_mutex.lock()
if region_id in regions:
collector_mutex.unlock()
return
regions[region_id] = {
'bbox': [x, y, w, h],
'raw_text': ""
'raw_text': "",
'widget': OCRRegionWidget(x, y, w, h, region_id)
}
collector_mutex.unlock()
_ensure_overlay()
def get_raw_text(region_id):
collector_mutex.lock()
@ -69,6 +88,8 @@ def _update_ocr_loop():
regions[rid]['raw_text'] = raw_text
collector_mutex.unlock()
print(f"OCR Text for {rid}: {raw_text}")
time.sleep(0.7)
def _preprocess_image(image):
@ -77,130 +98,87 @@ def _preprocess_image(image):
thresh = scaled.point(lambda p: 255 if p > 200 else 0)
return thresh.filter(ImageFilter.MedianFilter(3))
def _ensure_overlay():
"""
Creates the overlay window if none exists.
If no QApplication instance is running yet, schedule the creation after
the main application event loop starts (to avoid "Must construct a QApplication first" errors).
"""
global overlay_window
if overlay_window is not None:
return
class OCRRegionWidget(QWidget):
def __init__(self, x, y, w, h, region_id):
super().__init__()
# If there's already a running QApplication, create overlay immediately.
if QApplication.instance() is not None:
overlay_window = OverlayCanvas()
overlay_window.show()
else:
# Schedule creation for when the app event loop is up.
def delayed_create():
global overlay_window
if overlay_window is None:
overlay_window = OverlayCanvas()
overlay_window.show()
QTimer.singleShot(0, delayed_create)
class OverlayCanvas(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
screen_geo = QApplication.primaryScreen().geometry()
self.setGeometry(screen_geo)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setGeometry(x, y, w, h)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.setAttribute(Qt.WA_TransparentForMouseEvents, False)
self.drag_offset = None
self.selected_handle = None
self.selected_region_id = None
self.region_id = region_id
print(f"OCR Region Widget Created at {x}, {y}, {w}, {h}")
self.show()
def paintEvent(self, event):
painter = QPainter(self)
pen = QPen(QColor(0, 0, 255))
pen.setWidth(5)
pen.setWidth(3)
painter.setPen(pen)
collector_mutex.lock()
region_copy = {rid: data['bbox'][:] for rid, data in regions.items()}
collector_mutex.unlock()
# Draw main rectangle
painter.drawRect(0, 0, self.width(), self.height())
for rid, bbox in region_copy.items():
x, y, w, h = bbox
painter.drawRect(x, y, w, h)
painter.setFont(QFont("Arial", 12, QFont.Bold))
painter.setPen(QColor(0, 0, 255))
painter.drawText(x, y - 5, f"OCR Region: {rid}")
# Draw resize handles
painter.setBrush(QColor(0, 0, 255))
for handle in self._resize_handles():
painter.drawRect(handle)
def _resize_handles(self):
w, h = self.width(), self.height()
return [
QRect(0, 0, HANDLE_SIZE, HANDLE_SIZE), # Top-left
QRect(w - HANDLE_SIZE, h - HANDLE_SIZE, HANDLE_SIZE, HANDLE_SIZE) # Bottom-right
]
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
collector_mutex.lock()
all_items = list(regions.items())
collector_mutex.unlock()
for rid, data in all_items:
x, y, w, h = data['bbox']
handles = self._resize_handles(x, y, w, h)
for i, handle_rect in enumerate(handles):
if handle_rect.contains(event.pos()):
for i, handle in enumerate(self._resize_handles()):
if handle.contains(event.pos()):
self.selected_handle = i
self.selected_region_id = rid
return
if QRect(x, y, w, h).contains(event.pos()):
self.drag_offset = event.pos() - QPoint(x, y)
self.selected_region_id = rid
return
self.drag_offset = event.pos()
def mouseMoveEvent(self, event):
if not self.selected_region_id:
return
if self.selected_handle is not None:
w, h = self.width(), self.height()
if self.selected_handle == 0: # Top-left
new_w = w + (self.x() - event.globalX())
new_h = h + (self.y() - event.globalY())
new_x = event.globalX()
new_y = event.globalY()
if new_w < 20: new_w = 20
if new_h < 20: new_h = 20
self.setGeometry(new_x, new_y, new_w, new_h)
elif self.selected_handle == 1: # Bottom-right
new_w = event.globalX() - self.x()
new_h = event.globalY() - self.y()
if new_w < 20: new_w = 20
if new_h < 20: new_h = 20
self.setGeometry(self.x(), self.y(), new_w, new_h)
collector_mutex.lock()
if self.selected_region_id not in regions:
collector_mutex.unlock()
return
bbox = regions[self.selected_region_id]['bbox']
if self.region_id in regions:
regions[self.region_id]['bbox'] = [self.x(), self.y(), self.width(), self.height()]
collector_mutex.unlock()
x, y, w, h = bbox
if self.selected_handle is not None:
# resizing
if self.selected_handle == 0: # top-left
new_w = w + (x - event.x())
new_h = h + (y - event.y())
new_x = event.x()
new_y = event.y()
if new_w < 10: new_w = 10
if new_h < 10: new_h = 10
collector_mutex.lock()
if self.selected_region_id in regions:
regions[self.selected_region_id]['bbox'] = [new_x, new_y, new_w, new_h]
collector_mutex.unlock()
elif self.selected_handle == 1: # bottom-right
new_w = event.x() - x
new_h = event.y() - y
if new_w < 10: new_w = 10
if new_h < 10: new_h = 10
collector_mutex.lock()
if self.selected_region_id in regions:
regions[self.selected_region_id]['bbox'] = [x, y, new_w, new_h]
collector_mutex.unlock()
self.update()
elif self.drag_offset:
# dragging
new_x = event.x() - self.drag_offset.x()
new_y = event.y() - self.drag_offset.y()
new_x = event.globalX() - self.drag_offset.x()
new_y = event.globalY() - self.drag_offset.y()
self.move(new_x, new_y)
collector_mutex.lock()
if self.selected_region_id in regions:
regions[self.selected_region_id]['bbox'][0] = new_x
regions[self.selected_region_id]['bbox'][1] = new_y
if self.region_id in regions:
regions[self.region_id]['bbox'] = [new_x, new_y, self.width(), self.height()]
collector_mutex.unlock()
self.update()
def mouseReleaseEvent(self, event):
self.selected_handle = None
self.drag_offset = None
self.selected_region_id = None
def _resize_handles(self, x, y, w, h):
return [
QRect(x - HANDLE_SIZE//2, y - HANDLE_SIZE//2, HANDLE_SIZE, HANDLE_SIZE), # top-left
QRect(x + w - HANDLE_SIZE//2, y + h - HANDLE_SIZE//2, HANDLE_SIZE, HANDLE_SIZE) # bottom-right
]

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -1,46 +1,44 @@
#!/usr/bin/env python3
"""
Flyff Character Status Node (New Version):
- Has no inputs/outputs.
- Creates an OCR region in data_collector.
- Periodically grabs raw text from that region, parses it here in the node,
and sets data_manager's HP, MP, FP, EXP accordingly.
- Also updates its own text fields with the parsed values.
Flyff Character Status Node:
- Creates an OCR region in data_collector.
- Periodically grabs raw text from that region and updates status.
"""
import re
from OdenGraphQt import BaseNode
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtCore import QTimer # Corrected import
from Modules import data_manager, data_collector
class FlyffCharacterStatusNode(BaseNode):
__identifier__ = 'bunny-lab.io.flyff_character_status_node'
NODE_NAME = 'Flyff - Character Status'
__identifier__ = "bunny-lab.io.flyff_character_status_node"
NODE_NAME = "Flyff - Character Status"
def __init__(self):
super(FlyffCharacterStatusNode, self).__init__()
# Prevent duplicates
if data_manager.character_status_collector_exists:
QMessageBox.critical(None, "Error", "Only one Flyff Character Status Collector node is allowed.")
raise Exception("Duplicate Character Status Node.")
data_manager.character_status_collector_exists = True
# Add text fields for display
self.add_text_input('hp', 'HP', text="HP: 0/0")
self.add_text_input('mp', 'MP', text="MP: 0/0")
self.add_text_input('fp', 'FP', text="FP: 0/0")
self.add_text_input('exp', 'EXP', text="EXP: 0%")
self.add_text_input("hp", "HP", text="HP: 0/0")
self.add_text_input("mp", "MP", text="MP: 0/0")
self.add_text_input("fp", "FP", text="FP: 0/0")
self.add_text_input("exp", "EXP", text="EXP: 0%")
# Create a unique region id for this node (or just "character_status")
self.region_id = "character_status"
data_collector.create_ocr_region(self.region_id, x=250, y=50, w=180, h=130)
# Start the data_collector background thread (if not already started)
data_collector.start_collector()
# Set the node name
self.set_name("Flyff - Character Status")
# Set up a timer to periodically update character stats
self.timer = QTimer()
self.timer.timeout.connect(self.process_input)
self.timer.start(1000) # Update every second
def parse_character_stats(self, raw_text):
"""
Extract HP, MP, FP, EXP from the raw OCR text lines.
@ -52,6 +50,8 @@ class FlyffCharacterStatusNode(BaseNode):
exp_value = 0.0
if len(lines) >= 4:
print("Processing OCR Lines:", lines) # Debugging output
# line 1: HP
hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0])
if hp_match:
@ -82,15 +82,14 @@ class FlyffCharacterStatusNode(BaseNode):
def process_input(self):
"""
Called periodically by the global timer in your main application (borealis.py).
Called periodically to update character status from OCR.
"""
# Grab raw text from data_collector
raw_text = data_collector.get_raw_text(self.region_id)
print("Raw OCR Text:", raw_text) # Debugging OCR text reading
# Parse it
hp_c, hp_t, mp_c, mp_t, fp_c, fp_t, exp_v = self.parse_character_stats(raw_text)
# Update data_manager
# Update the data manager with the parsed values
data_manager.set_data_bulk({
"hp_current": hp_c,
"hp_total": hp_t,
@ -101,8 +100,8 @@ class FlyffCharacterStatusNode(BaseNode):
"exp": exp_v
})
# Update the node's text fields
self.set_property('hp', f"HP: {hp_c}/{hp_t}")
self.set_property('mp', f"MP: {mp_c}/{mp_t}")
self.set_property('fp', f"FP: {fp_c}/{fp_t}")
self.set_property('exp', f"EXP: {exp_v}%")
# Update the node's UI text fields
self.set_property("hp", f"HP: {hp_c}/{hp_t}")
self.set_property("mp", f"MP: {mp_c}/{mp_t}")
self.set_property("fp", f"FP: {fp_c}/{fp_t}")
self.set_property("exp", f"EXP: {exp_v}%")