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 threading
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import cv2
|
import cv2
|
||||||
import pytesseract
|
import pytesseract
|
||||||
from PIL import Image, ImageGrab, ImageFilter
|
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.QtCore import QRect, QPoint, Qt, QMutex, QTimer
|
||||||
from PyQt5.QtGui import QPainter, QPen, QColor, QFont
|
from PyQt5.QtGui import QPainter, QPen, QColor, QFont
|
||||||
|
|
||||||
@ -21,19 +22,37 @@ LABEL_HEIGHT = 20
|
|||||||
|
|
||||||
collector_mutex = QMutex()
|
collector_mutex = QMutex()
|
||||||
regions = {}
|
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):
|
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()
|
collector_mutex.lock()
|
||||||
if region_id in regions:
|
if region_id in regions:
|
||||||
collector_mutex.unlock()
|
collector_mutex.unlock()
|
||||||
return
|
return
|
||||||
regions[region_id] = {
|
regions[region_id] = {
|
||||||
'bbox': [x, y, w, h],
|
'bbox': [x, y, w, h],
|
||||||
'raw_text': ""
|
'raw_text': "",
|
||||||
|
'widget': OCRRegionWidget(x, y, w, h, region_id)
|
||||||
}
|
}
|
||||||
collector_mutex.unlock()
|
collector_mutex.unlock()
|
||||||
_ensure_overlay()
|
|
||||||
|
|
||||||
def get_raw_text(region_id):
|
def get_raw_text(region_id):
|
||||||
collector_mutex.lock()
|
collector_mutex.lock()
|
||||||
@ -69,6 +88,8 @@ def _update_ocr_loop():
|
|||||||
regions[rid]['raw_text'] = raw_text
|
regions[rid]['raw_text'] = raw_text
|
||||||
collector_mutex.unlock()
|
collector_mutex.unlock()
|
||||||
|
|
||||||
|
print(f"OCR Text for {rid}: {raw_text}")
|
||||||
|
|
||||||
time.sleep(0.7)
|
time.sleep(0.7)
|
||||||
|
|
||||||
def _preprocess_image(image):
|
def _preprocess_image(image):
|
||||||
@ -77,130 +98,87 @@ def _preprocess_image(image):
|
|||||||
thresh = scaled.point(lambda p: 255 if p > 200 else 0)
|
thresh = scaled.point(lambda p: 255 if p > 200 else 0)
|
||||||
return thresh.filter(ImageFilter.MedianFilter(3))
|
return thresh.filter(ImageFilter.MedianFilter(3))
|
||||||
|
|
||||||
def _ensure_overlay():
|
class OCRRegionWidget(QWidget):
|
||||||
"""
|
def __init__(self, x, y, w, h, region_id):
|
||||||
Creates the overlay window if none exists.
|
super().__init__()
|
||||||
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
|
|
||||||
|
|
||||||
# If there's already a running QApplication, create overlay immediately.
|
self.setGeometry(x, y, w, h)
|
||||||
if QApplication.instance() is not None:
|
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
|
||||||
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.setAttribute(Qt.WA_TranslucentBackground, True)
|
self.setAttribute(Qt.WA_TranslucentBackground, True)
|
||||||
|
self.setAttribute(Qt.WA_TransparentForMouseEvents, False)
|
||||||
|
|
||||||
self.drag_offset = None
|
self.drag_offset = None
|
||||||
self.selected_handle = 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):
|
def paintEvent(self, event):
|
||||||
painter = QPainter(self)
|
painter = QPainter(self)
|
||||||
pen = QPen(QColor(0, 0, 255))
|
pen = QPen(QColor(0, 0, 255))
|
||||||
pen.setWidth(5)
|
pen.setWidth(3)
|
||||||
painter.setPen(pen)
|
painter.setPen(pen)
|
||||||
|
|
||||||
collector_mutex.lock()
|
# Draw main rectangle
|
||||||
region_copy = {rid: data['bbox'][:] for rid, data in regions.items()}
|
painter.drawRect(0, 0, self.width(), self.height())
|
||||||
collector_mutex.unlock()
|
|
||||||
|
|
||||||
for rid, bbox in region_copy.items():
|
# Draw resize handles
|
||||||
x, y, w, h = bbox
|
painter.setBrush(QColor(0, 0, 255))
|
||||||
painter.drawRect(x, y, w, h)
|
for handle in self._resize_handles():
|
||||||
painter.setFont(QFont("Arial", 12, QFont.Bold))
|
painter.drawRect(handle)
|
||||||
painter.setPen(QColor(0, 0, 255))
|
|
||||||
painter.drawText(x, y - 5, f"OCR Region: {rid}")
|
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):
|
def mousePressEvent(self, event):
|
||||||
if event.button() == Qt.LeftButton:
|
if event.button() == Qt.LeftButton:
|
||||||
collector_mutex.lock()
|
for i, handle in enumerate(self._resize_handles()):
|
||||||
all_items = list(regions.items())
|
if handle.contains(event.pos()):
|
||||||
collector_mutex.unlock()
|
self.selected_handle = i
|
||||||
|
|
||||||
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()):
|
|
||||||
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
|
return
|
||||||
|
|
||||||
def mouseMoveEvent(self, event):
|
self.drag_offset = event.pos()
|
||||||
if not self.selected_region_id:
|
|
||||||
return
|
|
||||||
collector_mutex.lock()
|
|
||||||
if self.selected_region_id not in regions:
|
|
||||||
collector_mutex.unlock()
|
|
||||||
return
|
|
||||||
bbox = regions[self.selected_region_id]['bbox']
|
|
||||||
collector_mutex.unlock()
|
|
||||||
|
|
||||||
x, y, w, h = bbox
|
def mouseMoveEvent(self, event):
|
||||||
if self.selected_handle is not None:
|
if self.selected_handle is not None:
|
||||||
# resizing
|
w, h = self.width(), self.height()
|
||||||
if self.selected_handle == 0: # top-left
|
if self.selected_handle == 0: # Top-left
|
||||||
new_w = w + (x - event.x())
|
new_w = w + (self.x() - event.globalX())
|
||||||
new_h = h + (y - event.y())
|
new_h = h + (self.y() - event.globalY())
|
||||||
new_x = event.x()
|
new_x = event.globalX()
|
||||||
new_y = event.y()
|
new_y = event.globalY()
|
||||||
if new_w < 10: new_w = 10
|
if new_w < 20: new_w = 20
|
||||||
if new_h < 10: new_h = 10
|
if new_h < 20: new_h = 20
|
||||||
collector_mutex.lock()
|
self.setGeometry(new_x, new_y, new_w, new_h)
|
||||||
if self.selected_region_id in regions:
|
elif self.selected_handle == 1: # Bottom-right
|
||||||
regions[self.selected_region_id]['bbox'] = [new_x, new_y, new_w, new_h]
|
new_w = event.globalX() - self.x()
|
||||||
collector_mutex.unlock()
|
new_h = event.globalY() - self.y()
|
||||||
elif self.selected_handle == 1: # bottom-right
|
if new_w < 20: new_w = 20
|
||||||
new_w = event.x() - x
|
if new_h < 20: new_h = 20
|
||||||
new_h = event.y() - y
|
self.setGeometry(self.x(), self.y(), new_w, new_h)
|
||||||
if new_w < 10: new_w = 10
|
|
||||||
if new_h < 10: new_h = 10
|
collector_mutex.lock()
|
||||||
collector_mutex.lock()
|
if self.region_id in regions:
|
||||||
if self.selected_region_id in regions:
|
regions[self.region_id]['bbox'] = [self.x(), self.y(), self.width(), self.height()]
|
||||||
regions[self.selected_region_id]['bbox'] = [x, y, new_w, new_h]
|
collector_mutex.unlock()
|
||||||
collector_mutex.unlock()
|
|
||||||
self.update()
|
self.update()
|
||||||
elif self.drag_offset:
|
elif self.drag_offset:
|
||||||
# dragging
|
new_x = event.globalX() - self.drag_offset.x()
|
||||||
new_x = event.x() - self.drag_offset.x()
|
new_y = event.globalY() - self.drag_offset.y()
|
||||||
new_y = event.y() - self.drag_offset.y()
|
self.move(new_x, new_y)
|
||||||
|
|
||||||
collector_mutex.lock()
|
collector_mutex.lock()
|
||||||
if self.selected_region_id in regions:
|
if self.region_id in regions:
|
||||||
regions[self.selected_region_id]['bbox'][0] = new_x
|
regions[self.region_id]['bbox'] = [new_x, new_y, self.width(), self.height()]
|
||||||
regions[self.selected_region_id]['bbox'][1] = new_y
|
|
||||||
collector_mutex.unlock()
|
collector_mutex.unlock()
|
||||||
self.update()
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event):
|
def mouseReleaseEvent(self, event):
|
||||||
self.selected_handle = None
|
self.selected_handle = None
|
||||||
self.drag_offset = 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
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Flyff Character Status Node (New Version):
|
Flyff Character Status Node:
|
||||||
- Has no inputs/outputs.
|
- Creates an OCR region in data_collector.
|
||||||
- Creates an OCR region in data_collector.
|
- Periodically grabs raw text from that region and updates status.
|
||||||
- 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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from OdenGraphQt import BaseNode
|
from OdenGraphQt import BaseNode
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
from PyQt5.QtCore import QTimer # Corrected import
|
||||||
from Modules import data_manager, data_collector
|
from Modules import data_manager, data_collector
|
||||||
|
|
||||||
class FlyffCharacterStatusNode(BaseNode):
|
class FlyffCharacterStatusNode(BaseNode):
|
||||||
__identifier__ = 'bunny-lab.io.flyff_character_status_node'
|
__identifier__ = "bunny-lab.io.flyff_character_status_node"
|
||||||
NODE_NAME = 'Flyff - Character Status'
|
NODE_NAME = "Flyff - Character Status"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(FlyffCharacterStatusNode, self).__init__()
|
super(FlyffCharacterStatusNode, self).__init__()
|
||||||
# Prevent duplicates
|
|
||||||
if data_manager.character_status_collector_exists:
|
if data_manager.character_status_collector_exists:
|
||||||
QMessageBox.critical(None, "Error", "Only one Flyff Character Status Collector node is allowed.")
|
QMessageBox.critical(None, "Error", "Only one Flyff Character Status Collector node is allowed.")
|
||||||
raise Exception("Duplicate Character Status Node.")
|
raise Exception("Duplicate Character Status Node.")
|
||||||
data_manager.character_status_collector_exists = True
|
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('hp', 'HP', text="HP: 0/0")
|
self.add_text_input("mp", "MP", text="MP: 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('fp', 'FP', text="FP: 0/0")
|
self.add_text_input("exp", "EXP", text="EXP: 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"
|
self.region_id = "character_status"
|
||||||
data_collector.create_ocr_region(self.region_id, x=250, y=50, w=180, h=130)
|
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()
|
data_collector.start_collector()
|
||||||
|
|
||||||
# Set the node name
|
|
||||||
self.set_name("Flyff - Character Status")
|
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):
|
def parse_character_stats(self, raw_text):
|
||||||
"""
|
"""
|
||||||
Extract HP, MP, FP, EXP from the raw OCR text lines.
|
Extract HP, MP, FP, EXP from the raw OCR text lines.
|
||||||
@ -52,6 +50,8 @@ class FlyffCharacterStatusNode(BaseNode):
|
|||||||
exp_value = 0.0
|
exp_value = 0.0
|
||||||
|
|
||||||
if len(lines) >= 4:
|
if len(lines) >= 4:
|
||||||
|
print("Processing OCR Lines:", lines) # Debugging output
|
||||||
|
|
||||||
# line 1: HP
|
# line 1: HP
|
||||||
hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0])
|
hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0])
|
||||||
if hp_match:
|
if hp_match:
|
||||||
@ -82,15 +82,14 @@ class FlyffCharacterStatusNode(BaseNode):
|
|||||||
|
|
||||||
def process_input(self):
|
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)
|
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)
|
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({
|
data_manager.set_data_bulk({
|
||||||
"hp_current": hp_c,
|
"hp_current": hp_c,
|
||||||
"hp_total": hp_t,
|
"hp_total": hp_t,
|
||||||
@ -101,8 +100,8 @@ class FlyffCharacterStatusNode(BaseNode):
|
|||||||
"exp": exp_v
|
"exp": exp_v
|
||||||
})
|
})
|
||||||
|
|
||||||
# Update the node's text fields
|
# Update the node's UI text fields
|
||||||
self.set_property('hp', f"HP: {hp_c}/{hp_t}")
|
self.set_property("hp", f"HP: {hp_c}/{hp_t}")
|
||||||
self.set_property('mp', f"MP: {mp_c}/{mp_t}")
|
self.set_property("mp", f"MP: {mp_c}/{mp_t}")
|
||||||
self.set_property('fp', f"FP: {fp_c}/{fp_t}")
|
self.set_property("fp", f"FP: {fp_c}/{fp_t}")
|
||||||
self.set_property('exp', f"EXP: {exp_v}%")
|
self.set_property("exp", f"EXP: {exp_v}%")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user