Removed ReactJS Code
Removed new code from legacy codebase.
This commit is contained in:
398
Data/Modules/data_collector.py
Normal file
398
Data/Modules/data_collector.py
Normal file
@ -0,0 +1,398 @@
|
||||
# Modules/data_collector.py
|
||||
|
||||
import threading
|
||||
import time
|
||||
import re
|
||||
import sys
|
||||
import numpy as np
|
||||
import cv2
|
||||
import concurrent.futures
|
||||
|
||||
# Vision-related Imports
|
||||
import pytesseract
|
||||
import easyocr
|
||||
import torch
|
||||
|
||||
from PIL import Image, ImageGrab, ImageFilter
|
||||
|
||||
from PyQt5.QtWidgets import QApplication, QWidget
|
||||
from PyQt5.QtCore import QRect, QPoint, Qt, QMutex, QTimer
|
||||
from PyQt5.QtGui import QPainter, QPen, QColor, QFont
|
||||
|
||||
# Initialize EasyOCR with CUDA support
|
||||
reader_cpu = None
|
||||
reader_gpu = None
|
||||
|
||||
def initialize_ocr_engines():
|
||||
global reader_cpu, reader_gpu
|
||||
reader_cpu = easyocr.Reader(['en'], gpu=False)
|
||||
reader_gpu = easyocr.Reader(['en'], gpu=True if torch.cuda.is_available() else False)
|
||||
|
||||
pytesseract.pytesseract.tesseract_cmd = r"C:\\Program Files\\Tesseract-OCR\\tesseract.exe"
|
||||
|
||||
DEFAULT_WIDTH = 180
|
||||
DEFAULT_HEIGHT = 130
|
||||
HANDLE_SIZE = 5
|
||||
LABEL_HEIGHT = 20
|
||||
|
||||
collector_mutex = QMutex()
|
||||
regions = {}
|
||||
|
||||
app_instance = None
|
||||
|
||||
def _ensure_qapplication():
|
||||
"""
|
||||
Ensures that QApplication is initialized before creating widgets.
|
||||
Must be called from the main thread.
|
||||
"""
|
||||
global app_instance
|
||||
if app_instance is None:
|
||||
app_instance = QApplication(sys.argv) # Start in main thread
|
||||
|
||||
def capture_region_as_image(region_id):
|
||||
collector_mutex.lock()
|
||||
if region_id not in regions:
|
||||
collector_mutex.unlock()
|
||||
return None
|
||||
x, y, w, h = regions[region_id]['bbox'][:]
|
||||
collector_mutex.unlock()
|
||||
screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h))
|
||||
return screenshot
|
||||
|
||||
def create_ocr_region(region_id, x=250, y=50, w=DEFAULT_WIDTH, h=DEFAULT_HEIGHT, color=(255, 255, 0), thickness=2):
|
||||
"""
|
||||
Creates an OCR region with a visible, resizable box on the screen.
|
||||
Allows setting custom color (RGB) and line thickness.
|
||||
"""
|
||||
_ensure_qapplication()
|
||||
|
||||
collector_mutex.lock()
|
||||
if region_id in regions:
|
||||
collector_mutex.unlock()
|
||||
return
|
||||
regions[region_id] = {
|
||||
'bbox': [x, y, w, h],
|
||||
'raw_text': "",
|
||||
'widget': OCRRegionWidget(x, y, w, h, region_id, color, thickness)
|
||||
}
|
||||
collector_mutex.unlock()
|
||||
|
||||
def get_raw_text(region_id):
|
||||
collector_mutex.lock()
|
||||
if region_id not in regions:
|
||||
collector_mutex.unlock()
|
||||
return ""
|
||||
text = regions[region_id]['raw_text']
|
||||
collector_mutex.unlock()
|
||||
return text
|
||||
|
||||
def start_collector():
|
||||
initialize_ocr_engines()
|
||||
t = threading.Thread(target=_update_ocr_loop, daemon=True)
|
||||
t.start()
|
||||
|
||||
def _update_ocr_loop():
|
||||
while True:
|
||||
collector_mutex.lock()
|
||||
region_ids = list(regions.keys())
|
||||
collector_mutex.unlock()
|
||||
|
||||
for rid in region_ids:
|
||||
collector_mutex.lock()
|
||||
bbox = regions[rid]['bbox'][:]
|
||||
collector_mutex.unlock()
|
||||
|
||||
x, y, w, h = bbox
|
||||
screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h))
|
||||
processed = _preprocess_image(screenshot)
|
||||
raw_text = pytesseract.image_to_string(processed, config='--psm 6 --oem 1')
|
||||
|
||||
collector_mutex.lock()
|
||||
if rid in regions:
|
||||
regions[rid]['raw_text'] = raw_text
|
||||
collector_mutex.unlock()
|
||||
|
||||
time.sleep(0.7)
|
||||
|
||||
def _preprocess_image(image):
|
||||
gray = image.convert("L")
|
||||
scaled = gray.resize((gray.width * 3, gray.height * 3))
|
||||
thresh = scaled.point(lambda p: 255 if p > 200 else 0)
|
||||
return thresh.filter(ImageFilter.MedianFilter(3))
|
||||
|
||||
|
||||
def find_word_positions(region_id, word, offset_x=0, offset_y=0, margin=5, ocr_engine="CPU", num_slices=1):
|
||||
"""
|
||||
Uses user-defined horizontal slices and threading for faster inference.
|
||||
"""
|
||||
collector_mutex.lock()
|
||||
if region_id not in regions:
|
||||
collector_mutex.unlock()
|
||||
return []
|
||||
|
||||
bbox = regions[region_id]['bbox']
|
||||
collector_mutex.unlock()
|
||||
|
||||
x, y, w, h = bbox
|
||||
left, top, right, bottom = x, y, x + w, y + h
|
||||
|
||||
if right <= left or bottom <= top:
|
||||
print(f"[ERROR] Invalid OCR region bounds: {bbox}")
|
||||
return []
|
||||
|
||||
try:
|
||||
image = ImageGrab.grab(bbox=(left, top, right, bottom))
|
||||
orig_width, orig_height = image.size
|
||||
|
||||
word_positions = []
|
||||
|
||||
# Ensure number of slices does not exceed image height
|
||||
num_slices = min(num_slices, orig_height)
|
||||
strip_height = max(1, orig_height // num_slices)
|
||||
|
||||
def process_strip(strip_id):
|
||||
strip_y = strip_id * strip_height
|
||||
strip = image.crop((0, strip_y, orig_width, strip_y + strip_height))
|
||||
|
||||
strip_np = np.array(strip)
|
||||
|
||||
detected_positions = []
|
||||
if ocr_engine == "CPU":
|
||||
ocr_data = pytesseract.image_to_data(strip, config='--psm 6 --oem 1', output_type=pytesseract.Output.DICT)
|
||||
|
||||
for i in range(len(ocr_data['text'])):
|
||||
if re.search(rf"\b{word}\b", ocr_data['text'][i], re.IGNORECASE):
|
||||
x_scaled = int(ocr_data['left'][i])
|
||||
y_scaled = int(ocr_data['top'][i]) + strip_y
|
||||
w_scaled = int(ocr_data['width'][i])
|
||||
h_scaled = int(ocr_data['height'][i])
|
||||
|
||||
detected_positions.append((x_scaled + offset_x, y_scaled + offset_y, w_scaled + (margin * 2), h_scaled + (margin * 2)))
|
||||
|
||||
else:
|
||||
results = reader_gpu.readtext(strip_np)
|
||||
for (bbox, text, _) in results:
|
||||
if re.search(rf"\b{word}\b", text, re.IGNORECASE):
|
||||
(x_min, y_min), (x_max, y_max) = bbox[0], bbox[2]
|
||||
|
||||
x_scaled = int(x_min)
|
||||
y_scaled = int(y_min) + strip_y
|
||||
w_scaled = int(x_max - x_min)
|
||||
h_scaled = int(y_max - y_min)
|
||||
|
||||
detected_positions.append((x_scaled + offset_x, y_scaled + offset_y, w_scaled + (margin * 2), h_scaled + (margin * 2)))
|
||||
|
||||
return detected_positions
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=num_slices) as executor:
|
||||
strip_results = list(executor.map(process_strip, range(num_slices)))
|
||||
|
||||
for strip_result in strip_results:
|
||||
word_positions.extend(strip_result)
|
||||
|
||||
return word_positions
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to capture OCR region: {e}")
|
||||
return []
|
||||
|
||||
def draw_identification_boxes(region_id, positions, color=(0, 0, 255), thickness=2):
|
||||
"""
|
||||
Draws non-interactive rectangles at specified positions within the given OCR region.
|
||||
Uses a separate rendering thread to prevent blocking OCR processing.
|
||||
"""
|
||||
collector_mutex.lock()
|
||||
if region_id in regions and 'widget' in regions[region_id]:
|
||||
widget = regions[region_id]['widget']
|
||||
widget.update_draw_positions(positions, color, thickness)
|
||||
collector_mutex.unlock()
|
||||
|
||||
def update_region_slices(region_id, num_slices):
|
||||
"""
|
||||
Updates the number of visual slices in the OCR region.
|
||||
"""
|
||||
collector_mutex.lock()
|
||||
if region_id in regions and 'widget' in regions[region_id]:
|
||||
widget = regions[region_id]['widget']
|
||||
widget.set_num_slices(num_slices)
|
||||
collector_mutex.unlock()
|
||||
|
||||
class OCRRegionWidget(QWidget):
|
||||
def __init__(self, x, y, w, h, region_id, color, thickness):
|
||||
super().__init__()
|
||||
|
||||
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.region_id = region_id
|
||||
self.box_color = QColor(*color)
|
||||
self.line_thickness = thickness
|
||||
self.draw_positions = []
|
||||
self.previous_positions = [] # This prevents redundant redraws
|
||||
self.num_slices = 1 # Ensures slice count is initialized
|
||||
|
||||
# --- Initialization for interactive handles ---
|
||||
self.selected_handle = None # Tracks which handle is being dragged/resized
|
||||
self.drag_offset = None # Tracks the offset for moving the widget
|
||||
|
||||
self.show()
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
pen = QPen(self.box_color)
|
||||
pen.setWidth(self.line_thickness)
|
||||
painter.setPen(pen)
|
||||
|
||||
# Draw main rectangle
|
||||
painter.drawRect(0, 0, self.width(), self.height())
|
||||
|
||||
# Draw detected word overlays
|
||||
for x, y, w, h in self.draw_positions:
|
||||
painter.drawRect(x, y, w, h)
|
||||
|
||||
# Draw faint slice division lines
|
||||
if self.num_slices > 1:
|
||||
strip_height = self.height() // self.num_slices
|
||||
pen.setColor(QColor(150, 150, 150, 100)) # Light gray, semi-transparent
|
||||
pen.setWidth(1)
|
||||
painter.setPen(pen)
|
||||
|
||||
for i in range(1, self.num_slices): # Do not draw the last one at the bottom
|
||||
painter.drawLine(0, i * strip_height, self.width(), i * strip_height)
|
||||
|
||||
# --- Draw interactive handles (grabbers) with reduced opacity (15%) ---
|
||||
# 15% opacity of 255 is approximately 38
|
||||
handle_color = QColor(0, 0, 0, 50)
|
||||
for handle in self._resize_handles():
|
||||
painter.fillRect(handle, handle_color)
|
||||
painter.drawRect(handle) # Optional: draw a border around the handle
|
||||
|
||||
def set_draw_positions(self, positions, color, thickness):
|
||||
"""
|
||||
Updates the overlay positions and visual settings.
|
||||
"""
|
||||
self.draw_positions = positions
|
||||
self.box_color = QColor(*color)
|
||||
self.line_thickness = thickness
|
||||
self.update()
|
||||
|
||||
def update_draw_positions(self, positions, color, thickness):
|
||||
"""
|
||||
Updates the overlay positions and redraws only if the positions have changed.
|
||||
This prevents unnecessary flickering.
|
||||
"""
|
||||
if positions == self.previous_positions:
|
||||
return # No change, do not update
|
||||
|
||||
self.previous_positions = positions # Store last known positions
|
||||
self.draw_positions = positions
|
||||
self.box_color = QColor(*color)
|
||||
self.line_thickness = thickness
|
||||
self.update() # Redraw only if needed
|
||||
|
||||
def set_num_slices(self, num_slices):
|
||||
"""
|
||||
Updates the number of horizontal slices for visualization.
|
||||
"""
|
||||
self.num_slices = num_slices
|
||||
self.update()
|
||||
|
||||
def _resize_handles(self):
|
||||
"""
|
||||
Returns a list of QRect objects representing the interactive handles:
|
||||
- Index 0: Top-left (resize)
|
||||
- Index 1: Top-right (resize)
|
||||
- Index 2: Bottom-left (resize)
|
||||
- Index 3: Bottom-right (resize)
|
||||
- Index 4: Top-center (dragger)
|
||||
"""
|
||||
w, h = self.width(), self.height()
|
||||
handles = [
|
||||
QRect(0, 0, HANDLE_SIZE, HANDLE_SIZE), # Top-left
|
||||
QRect(w - HANDLE_SIZE, 0, HANDLE_SIZE, HANDLE_SIZE), # Top-right
|
||||
QRect(0, h - HANDLE_SIZE, HANDLE_SIZE, HANDLE_SIZE), # Bottom-left
|
||||
QRect(w - HANDLE_SIZE, h - HANDLE_SIZE, HANDLE_SIZE, HANDLE_SIZE) # Bottom-right
|
||||
]
|
||||
# Top-center handle: centered along the top edge
|
||||
top_center_x = (w - HANDLE_SIZE) // 2
|
||||
top_center = QRect(top_center_x, 0, HANDLE_SIZE, HANDLE_SIZE)
|
||||
handles.append(top_center)
|
||||
return handles
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.LeftButton:
|
||||
# Check if any handle (including the new top-center) is clicked
|
||||
for i, handle in enumerate(self._resize_handles()):
|
||||
if handle.contains(event.pos()):
|
||||
self.selected_handle = i
|
||||
# For the top-center handle (index 4), initialize drag offset for moving
|
||||
if i == 4:
|
||||
self.drag_offset = event.pos()
|
||||
return
|
||||
# If no handle is clicked, allow dragging by clicking anywhere in the widget
|
||||
self.drag_offset = event.pos()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self.selected_handle is not None:
|
||||
if self.selected_handle == 4:
|
||||
# --- Top-center handle dragging ---
|
||||
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.region_id in regions:
|
||||
regions[self.region_id]["bbox"] = [new_x, new_y, self.width(), self.height()]
|
||||
collector_mutex.unlock()
|
||||
self.update()
|
||||
else:
|
||||
# --- Resizing logic for corner handles ---
|
||||
if self.selected_handle == 0: # Top-left
|
||||
new_w = self.width() + (self.x() - event.globalX())
|
||||
new_h = self.height() + (self.y() - event.globalY())
|
||||
new_x = event.globalX()
|
||||
new_y = event.globalY()
|
||||
elif self.selected_handle == 1: # Top-right
|
||||
new_w = event.globalX() - self.x()
|
||||
new_h = self.height() + (self.y() - event.globalY())
|
||||
new_x = self.x()
|
||||
new_y = event.globalY()
|
||||
elif self.selected_handle == 2: # Bottom-left
|
||||
new_w = self.width() + (self.x() - event.globalX())
|
||||
new_h = event.globalY() - self.y()
|
||||
new_x = event.globalX()
|
||||
new_y = self.y()
|
||||
elif self.selected_handle == 3: # Bottom-right
|
||||
new_w = event.globalX() - self.x()
|
||||
new_h = event.globalY() - self.y()
|
||||
new_x = self.x()
|
||||
new_y = self.y()
|
||||
|
||||
if new_w < 20:
|
||||
new_w = 20
|
||||
if new_h < 20:
|
||||
new_h = 20
|
||||
|
||||
self.setGeometry(new_x, new_y, new_w, new_h)
|
||||
collector_mutex.lock()
|
||||
if self.region_id in regions:
|
||||
regions[self.region_id]["bbox"] = [self.x(), self.y(), self.width(), self.height()]
|
||||
collector_mutex.unlock()
|
||||
self.update()
|
||||
elif self.drag_offset:
|
||||
# --- General widget dragging (if no handle was clicked) ---
|
||||
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.region_id in regions:
|
||||
regions[self.region_id]["bbox"] = [new_x, new_y, self.width(), self.height()]
|
||||
collector_mutex.unlock()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""
|
||||
Resets the drag/resize state once the mouse button is released.
|
||||
"""
|
||||
self.selected_handle = None
|
||||
self.drag_offset = None
|
156
Data/Modules/data_manager.py
Normal file
156
Data/Modules/data_manager.py
Normal file
@ -0,0 +1,156 @@
|
||||
import threading
|
||||
import time
|
||||
import base64
|
||||
from flask import Flask, jsonify, Response
|
||||
from PyQt5.QtCore import QMutex
|
||||
|
||||
# Global datastore for character metrics
|
||||
data_store = {
|
||||
"hp_current": 0,
|
||||
"hp_total": 0,
|
||||
"mp_current": 0,
|
||||
"mp_total": 0,
|
||||
"fp_current": 0,
|
||||
"fp_total": 0,
|
||||
"exp": 0.0
|
||||
}
|
||||
|
||||
# Mutex for thread safety
|
||||
data_mutex = QMutex()
|
||||
|
||||
# Flag to ensure only one character status collector node exists
|
||||
character_status_collector_exists = False
|
||||
|
||||
# A place to store the screenshot in base64
|
||||
status_screenshot_base64 = ""
|
||||
|
||||
# Flask Application
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/data')
|
||||
def data_api():
|
||||
"""
|
||||
Returns the current character metrics as JSON.
|
||||
"""
|
||||
return jsonify(get_data())
|
||||
|
||||
@app.route('/exp')
|
||||
def exp_api():
|
||||
"""
|
||||
Returns the EXP data.
|
||||
"""
|
||||
return jsonify({"exp": get_data()["exp"]})
|
||||
|
||||
@app.route('/hp')
|
||||
def hp_api():
|
||||
"""
|
||||
Returns the HP data.
|
||||
"""
|
||||
return jsonify({
|
||||
"hp_current": get_data()["hp_current"],
|
||||
"hp_total": get_data()["hp_total"]
|
||||
})
|
||||
|
||||
@app.route('/mp')
|
||||
def mp_api():
|
||||
"""
|
||||
Returns the MP data.
|
||||
"""
|
||||
return jsonify({
|
||||
"mp_current": get_data()["mp_current"],
|
||||
"mp_total": get_data()["mp_total"]
|
||||
})
|
||||
|
||||
@app.route('/fp')
|
||||
def fp_api():
|
||||
"""
|
||||
Returns the FP data.
|
||||
"""
|
||||
return jsonify({
|
||||
"fp_current": get_data()["fp_current"],
|
||||
"fp_total": get_data()["fp_total"]
|
||||
})
|
||||
|
||||
@app.route('/flyff/status')
|
||||
def status_screenshot():
|
||||
"""
|
||||
Returns an HTML page that displays the stored screenshot and
|
||||
automatically refreshes it every second without requiring a manual page reload.
|
||||
"""
|
||||
html = """
|
||||
<html>
|
||||
<head>
|
||||
<title>Borealis - Live Status</title>
|
||||
<script>
|
||||
// Reload the <img> every second
|
||||
setInterval(function(){
|
||||
var img = document.getElementById('status_img');
|
||||
img.src = '/flyff/status_rawdata?random=' + Math.random();
|
||||
}, 1000);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<img id="status_img" src="/flyff/status_rawdata" />
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return html
|
||||
|
||||
@app.route('/flyff/status_rawdata')
|
||||
def status_screenshot_data():
|
||||
"""
|
||||
Serves the raw PNG bytes (decoded from base64) used by <img> in /status_screenshot.
|
||||
"""
|
||||
data_mutex.lock()
|
||||
encoded = status_screenshot_base64
|
||||
data_mutex.unlock()
|
||||
|
||||
if not encoded:
|
||||
# No image captured yet, return HTTP 204 "No Content"
|
||||
return "", 204
|
||||
|
||||
raw_img = base64.b64decode(encoded)
|
||||
return Response(raw_img, mimetype='image/png')
|
||||
|
||||
def start_api_server():
|
||||
"""
|
||||
Starts the Flask API server in a separate daemon thread.
|
||||
"""
|
||||
def run():
|
||||
app.run(host="0.0.0.0", port=5000) # Allows external connections
|
||||
t = threading.Thread(target=run, daemon=True)
|
||||
t.start()
|
||||
|
||||
def get_data():
|
||||
"""
|
||||
Return a copy of the global data_store.
|
||||
"""
|
||||
data_mutex.lock()
|
||||
data_copy = data_store.copy()
|
||||
data_mutex.unlock()
|
||||
return data_copy
|
||||
|
||||
def set_data(key, value):
|
||||
"""
|
||||
Set a single metric in the global data_store.
|
||||
"""
|
||||
data_mutex.lock()
|
||||
data_store[key] = value
|
||||
data_mutex.unlock()
|
||||
|
||||
def set_data_bulk(metrics_dict):
|
||||
"""
|
||||
Update multiple metrics in the global data_store at once.
|
||||
"""
|
||||
data_mutex.lock()
|
||||
data_store.update(metrics_dict)
|
||||
data_mutex.unlock()
|
||||
|
||||
def set_status_screenshot(encoded_str):
|
||||
"""
|
||||
Called by the OCR node to store the base64-encoded screenshot data.
|
||||
"""
|
||||
global status_screenshot_base64
|
||||
data_mutex.lock()
|
||||
status_screenshot_base64 = encoded_str
|
||||
data_mutex.unlock()
|
Reference in New Issue
Block a user