Fixed On-Screen OCR region Overlay
This commit is contained in:
parent
e30ba4ec4f
commit
6888b55612
Binary file not shown.
@ -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
|
||||
]
|
||||
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
@ -1,46 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Flyff Character Status Node (New Version):
|
||||
- Has no inputs/outputs.
|
||||
Flyff Character Status Node:
|
||||
- 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.
|
||||
- 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}%")
|
||||
|
Loading…
x
Reference in New Issue
Block a user