Fully Functional (Minus Region Resizing)

This commit is contained in:
Nicole Rappe 2025-02-12 03:15:39 -07:00
parent e25b04f8cb
commit ff313f650c
5 changed files with 328 additions and 12 deletions

@ -1,15 +1,4 @@
#!/usr/bin/env python3
"""
Main Application (flow_UI.py)
This file dynamically imports custom node classes from the 'Nodes' package,
registers them with NodeGraphQt, and sets up an empty graph.
Nodes can be added dynamically via the graphs right-click context menu,
and a "Remove Selected Node" option is provided.
A global update timer periodically calls process_input() on nodes.
Additionally, this file patches QGraphicsScene.setSelectionArea to handle
selection behavior properly (so that multiple nodes can be selected).
"""
# --- Patch QGraphicsScene.setSelectionArea to handle selection arguments ---
from Qt import QtWidgets, QtCore, QtGui
@ -73,7 +62,7 @@ if __name__ == '__main__':
# Create the NodeGraph controller.
graph = NodeGraph()
graph.widget.setWindowTitle("Modular Nodes Demo")
graph.widget.setWindowTitle("Project Borealis - Flyff Information Overlay")
# Dynamically import custom node classes from the 'Nodes' package.
custom_nodes = import_nodes_from_folder('Nodes')

327
data_collector_v2.py Normal file

@ -0,0 +1,327 @@
#!/usr/bin/env python3
import time
import re
import threading
import numpy as np
import cv2
import pytesseract
from flask import Flask, jsonify
from PIL import Image, ImageGrab, ImageFilter
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import QTimer, QRect, QPoint, Qt, QMutex
from PyQt5.QtGui import QPainter, QPen, QColor, QFont
pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
# =============================================================================
# Global Config
# =============================================================================
POLLING_RATE_MS = 500
MAX_DATA_POINTS = 8
DEFAULT_WIDTH = 180
DEFAULT_HEIGHT = 130
HANDLE_SIZE = 8
LABEL_HEIGHT = 20
# Template Matching Threshold (Define it here)
MATCH_THRESHOLD = 0.4 # Set to 0.4 as a typical value for correlation threshold
# Flask API setup
app = Flask(__name__)
# **Shared Region Data (Thread-Safe)**
region_lock = QMutex() # Mutex to synchronize access between UI and OCR thread
shared_region = {
"x": 250,
"y": 50,
"w": DEFAULT_WIDTH,
"h": DEFAULT_HEIGHT
}
# Global variable for OCR data
latest_data = {
"hp_current": 0,
"hp_total": 0,
"mp_current": 0,
"mp_total": 0,
"fp_current": 0,
"fp_total": 0,
"exp": 0.0000
}
# =============================================================================
# OCR Data Collection
# =============================================================================
def preprocess_image(image):
"""
Preprocess the image for OCR: convert to grayscale, resize, and apply thresholding.
"""
gray = image.convert("L") # Convert to grayscale
scaled = gray.resize((gray.width * 3, gray.height * 3)) # Upscale the image for better accuracy
thresh = scaled.point(lambda p: p > 200 and 255) # Apply a threshold to clean up the image
return thresh.filter(ImageFilter.MedianFilter(3)) # Apply a median filter to remove noise
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 locate_bars_opencv(template_path, threshold=MATCH_THRESHOLD):
"""
Attempt to locate the bars via OpenCV template matching.
"""
screenshot_pil = ImageGrab.grab()
screenshot_np = np.array(screenshot_pil)
screenshot_bgr = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR)
template_bgr = cv2.imread(template_path, cv2.IMREAD_COLOR)
if template_bgr is None:
return None
result = cv2.matchTemplate(screenshot_bgr, template_bgr, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
th, tw, _ = template_bgr.shape
if max_val >= threshold:
found_x, found_y = max_loc
return (found_x, found_y, tw, th)
else:
return None
def parse_all_stats(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
# =============================================================================
# Region & UI
# =============================================================================
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),
]
# =============================================================================
# Flask API Server
# =============================================================================
@app.route('/data')
def get_data():
"""Returns the latest OCR data as JSON."""
return jsonify(latest_data)
def collect_ocr_data():
"""
Collects OCR data every 0.5 seconds and updates global latest_data.
"""
global latest_data
while True:
# **Fetch updated region values from UI (thread-safe)**
region_lock.lock() # Lock for thread safety
x, y, w, h = shared_region["x"], shared_region["y"], shared_region["w"], shared_region["h"]
region_lock.unlock()
# **Grab the image of the updated region**
screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h))
# **Debug: Save screenshots to verify capture**
screenshot.save("debug_screenshot.png")
# Preprocess image
processed = preprocess_image(screenshot)
processed.save("debug_processed.png") # Debug: Save processed image
# Run OCR
text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1')
print("OCR Output:", text) # Debugging
stats = parse_all_stats(text.strip())
hp_cur, hp_max = stats["hp"]
mp_cur, mp_max = stats["mp"]
fp_cur, fp_max = stats["fp"]
exp_val = stats["exp"]
# Update latest data
latest_data = {
"hp_current": hp_cur,
"hp_total": hp_max,
"mp_current": mp_cur,
"mp_total": mp_max,
"fp_current": fp_cur,
"fp_total": fp_max,
"exp": exp_val
}
print(f"OCR Updated: HP: {hp_cur}/{hp_max}, MP: {mp_cur}/{mp_max}, FP: {fp_cur}/{fp_max}, EXP: {exp_val}") # Debug
time.sleep(0.5)
# =============================================================================
# OverlayCanvas (UI)
# =============================================================================
class OverlayCanvas(QWidget):
"""
UI overlay that allows dragging/resizing of the OCR region.
"""
def __init__(self, parent=None):
super().__init__(parent)
# **Full-screen overlay**
screen_geo = QApplication.primaryScreen().geometry()
self.setGeometry(screen_geo) # Set to full screen
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground, True)
# **Load shared region**
self.region = shared_region
self.drag_offset = None
self.selected_handle = None
def paintEvent(self, event):
"""Draw the blue OCR region."""
painter = QPainter(self)
pen = QPen(QColor(0, 0, 255))
pen.setWidth(5) # Thicker lines
painter.setPen(pen)
painter.drawRect(self.region["x"], self.region["y"], self.region["w"], self.region["h"])
painter.setFont(QFont("Arial", 12, QFont.Bold))
painter.setPen(QColor(0, 0, 255))
painter.drawText(self.region["x"], self.region["y"] - 5, "Character Status")
def mousePressEvent(self, event):
"""Handle drag and resize interactions."""
if event.button() == Qt.LeftButton:
region_lock.lock() # Lock for thread safety
x, y, w, h = self.region["x"], self.region["y"], self.region["w"], self.region["h"]
region_lock.unlock()
for i, handle in enumerate(self.resize_handles()):
if handle.contains(event.pos()):
self.selected_handle = i
return
if QRect(x, y, w, h).contains(event.pos()):
self.drag_offset = event.pos() - QPoint(x, y)
def mouseMoveEvent(self, event):
"""Allow dragging and resizing."""
if self.selected_handle is not None:
region_lock.lock()
sr = self.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: # 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)
region_lock.unlock()
self.update()
elif self.drag_offset:
region_lock.lock()
self.region["x"] = event.x() - self.drag_offset.x()
self.region["y"] = event.y() - self.drag_offset.y()
region_lock.unlock()
print(f"Region Moved: x={self.region['x']}, y={self.region['y']}, w={self.region['w']}, h={self.region['h']}") # Debugging
self.update()
def mouseReleaseEvent(self, event):
"""End drag or resize event."""
self.selected_handle = None
self.drag_offset = None
def resize_handles(self):
"""Get the resizing handles of the region."""
return [
QRect(self.region["x"] - HANDLE_SIZE // 2, self.region["y"] - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
QRect(self.region["x"] + self.region["w"] - HANDLE_SIZE // 2, self.region["y"] + self.region["h"] - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE)
]
# =============================================================================
# Start Application
# =============================================================================
def run_flask_app():
"""Runs the Flask API server in a separate thread."""
app.run(host="127.0.0.1", port=5000)
if __name__ == '__main__':
# Start the OCR thread
collector_thread = threading.Thread(target=collect_ocr_data, daemon=True)
collector_thread.start()
# Start the Flask API thread
flask_thread = threading.Thread(target=run_flask_app, daemon=True)
flask_thread.start()
# Start PyQt5 GUI
app_gui = QApplication([])
overlay_window = OverlayCanvas()
overlay_window.show()
# Run event loop
app_gui.exec_()

BIN
debug_processed.png Normal file

Binary file not shown.

After

(image error) Size: 2.8 KiB

BIN
debug_screenshot.png Normal file

Binary file not shown.

After

(image error) Size: 13 KiB