diff --git a/borealis_overlay.py b/Legacy_Code/borealis_overlay.py similarity index 100% rename from borealis_overlay.py rename to Legacy_Code/borealis_overlay.py diff --git a/flow_UI.py b/borealis.py similarity index 87% rename from flow_UI.py rename to borealis.py index ea6f2a9..d9c4e3b 100644 --- a/flow_UI.py +++ b/borealis.py @@ -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 graph’s 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') diff --git a/data_collector_v2.py b/data_collector_v2.py new file mode 100644 index 0000000..abeb253 --- /dev/null +++ b/data_collector_v2.py @@ -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_() diff --git a/debug_processed.png b/debug_processed.png new file mode 100644 index 0000000..981699c Binary files /dev/null and b/debug_processed.png differ diff --git a/debug_screenshot.png b/debug_screenshot.png new file mode 100644 index 0000000..a69d3d1 Binary files /dev/null and b/debug_screenshot.png differ