Update borealis_overlay.py
This commit is contained in:
parent
943967be60
commit
78a3643742
@ -1,21 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Project Borealis (Fixed Progress Bar)
|
||||
=====================================
|
||||
• Uses Tesseract OCR only, storing up to 7 data points.
|
||||
• Rollover logic (reset if new < old).
|
||||
• Skips duplicates.
|
||||
• Shows a Rich table of historical data (2+ points).
|
||||
• Then displays a Rich progress bar for the current EXP, plus predicted time to level
|
||||
in hours/min/seconds if over 60 minutes.
|
||||
|
||||
Key fix: we use "with Progress(...) as progress:" *without* transient,
|
||||
so the bar remains in the console after the update.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import pytesseract
|
||||
import easyocr
|
||||
import numpy as np
|
||||
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QCheckBox, QTextEdit, QComboBox, QVBoxLayout
|
||||
|
||||
from PyQt5.QtWidgets import QApplication, QWidget
|
||||
from PyQt5.QtCore import Qt, QRect, QPoint, QTimer
|
||||
from PyQt5.QtGui import QPainter, QPen, QColor, QFont
|
||||
from PIL import Image, ImageGrab, ImageEnhance, ImageFilter # For screen capture and image processing
|
||||
from PIL import Image, ImageGrab, ImageEnhance, ImageFilter
|
||||
|
||||
HANDLE_SIZE = 10 # Size of the resize handle squares
|
||||
LABEL_HEIGHT = 20 # Height of the label area above the rectangle
|
||||
DEFAULT_WIDTH = 150 # Default width for the regions
|
||||
DEFAULT_HEIGHT = 50 # Default height for the regions
|
||||
DEFAULT_SPACING = 20 # Default horizontal spacing between regions
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.progress import Progress, BarColumn, TextColumn
|
||||
|
||||
# Set the path for tesseract manually
|
||||
# ---------------- [ Global Config ] ----------------
|
||||
pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
|
||||
|
||||
OCR_ENGINE = "Tesseract" # Hard-coded since we removed EasyOCR
|
||||
POLLING_RATE_MS = 1000
|
||||
MAX_DATA_POINTS = 7
|
||||
|
||||
GREEN_HEADER_STYLE = "bold green"
|
||||
HANDLE_SIZE = 10
|
||||
LABEL_HEIGHT = 20
|
||||
DEFAULT_WIDTH = 150
|
||||
DEFAULT_HEIGHT = 50
|
||||
|
||||
def sanitize_experience_string(raw_text):
|
||||
"""Extract a float from raw OCR text, removing extraneous symbols."""
|
||||
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))
|
||||
return round(val, 4)
|
||||
|
||||
def format_experience_value(value):
|
||||
"""Format float to 'XX.XXXX' with leading zeros if needed."""
|
||||
if value < 0:
|
||||
value = 0
|
||||
elif value > 100:
|
||||
value = 100
|
||||
|
||||
float_4 = round(value, 4)
|
||||
raw_str = f"{float_4:.4f}"
|
||||
int_part, dec_part = raw_str.split('.')
|
||||
if int_part == "100":
|
||||
pass
|
||||
elif len(int_part) == 1 and int_part != "0":
|
||||
int_part = "0" + int_part
|
||||
elif int_part == "0":
|
||||
int_part = "00"
|
||||
return f"{int_part}.{dec_part}"
|
||||
|
||||
def format_duration(seconds):
|
||||
"""
|
||||
Convert total seconds into hours / min / sec:
|
||||
- If hours > 0 => "Xh Ym Zs"
|
||||
- Otherwise => "Xm Ys"
|
||||
"""
|
||||
seconds = int(seconds)
|
||||
hours = seconds // 3600
|
||||
leftover = seconds % 3600
|
||||
mins = leftover // 60
|
||||
secs = leftover % 60
|
||||
|
||||
if hours > 0:
|
||||
return f"{hours}h {mins}m {secs}s"
|
||||
else:
|
||||
return f"{mins}m {secs}s"
|
||||
|
||||
class Region:
|
||||
def __init__(self, x, y, label="Region", color=QColor(0, 0, 255)):
|
||||
@ -25,81 +96,68 @@ class Region:
|
||||
self.h = DEFAULT_HEIGHT
|
||||
self.label = label
|
||||
self.color = color
|
||||
self.visible = True # Track the visibility of the region
|
||||
self.data = "" # Store OCR data for this region
|
||||
self.visible = True
|
||||
self.data = ""
|
||||
|
||||
def rect(self):
|
||||
return QRect(self.x, self.y, self.w, self.h)
|
||||
|
||||
def label_rect(self):
|
||||
"""The rectangle representing the label area."""
|
||||
return QRect(self.x, self.y - LABEL_HEIGHT, self.w, LABEL_HEIGHT)
|
||||
|
||||
def resize_handles(self):
|
||||
"""Calculate the positions of the resize handles."""
|
||||
return [
|
||||
QRect(self.x - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), # Top-left
|
||||
QRect(self.x + self.w - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), # Top-right
|
||||
QRect(self.x - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), # Bottom-left
|
||||
QRect(self.x + self.w - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), # Bottom-right
|
||||
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),
|
||||
]
|
||||
|
||||
|
||||
class OverlayCanvas(QWidget):
|
||||
"""
|
||||
A canvas to draw, resize, and interact with rectangular regions.
|
||||
"""
|
||||
def __init__(self, regions, parent=None):
|
||||
super().__init__(parent)
|
||||
self.regions = regions
|
||||
self.edit_mode = False
|
||||
self.edit_mode = True
|
||||
self.selected_region = None
|
||||
self.selected_handle = None # Track which handle is being dragged
|
||||
self.selected_handle = None
|
||||
self.drag_offset = QPoint()
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
for region in self.regions:
|
||||
if region.visible: # Only draw visible regions
|
||||
# Draw the rectangle
|
||||
if region.visible:
|
||||
pen = QPen(region.color)
|
||||
pen.setWidth(3)
|
||||
painter.setPen(pen)
|
||||
painter.drawRect(region.x, region.y, region.w, region.h)
|
||||
|
||||
# Draw the label above the rectangle, aligned with the left edge of the region
|
||||
painter.setFont(QFont("Arial", 12, QFont.Bold))
|
||||
painter.setPen(region.color)
|
||||
painter.drawText(region.x, region.y - 5, region.label) # Aligned to the left of the region
|
||||
painter.drawText(region.x, region.y - 5, region.label)
|
||||
|
||||
# Draw resize handles if in edit mode
|
||||
if self.edit_mode:
|
||||
for handle in region.resize_handles():
|
||||
painter.fillRect(handle, region.color)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if not self.edit_mode:
|
||||
return # Ignore clicks if not in edit mode
|
||||
return
|
||||
|
||||
if event.button() == Qt.LeftButton:
|
||||
for region in reversed(self.regions): # Check regions from topmost to bottommost
|
||||
# Check if a resize handle is clicked
|
||||
for region in reversed(self.regions):
|
||||
# Check handles
|
||||
for i, handle in enumerate(region.resize_handles()):
|
||||
if handle.contains(event.pos()):
|
||||
self.selected_region = region
|
||||
self.selected_handle = i
|
||||
return
|
||||
|
||||
# Check if the label area is clicked (for dragging)
|
||||
# Check label or rect
|
||||
if region.label_rect().contains(event.pos()):
|
||||
self.selected_region = region
|
||||
self.selected_handle = None # No resize handle, just dragging the rectangle
|
||||
self.selected_handle = None
|
||||
self.drag_offset = event.pos() - QPoint(region.x, region.y)
|
||||
return
|
||||
|
||||
# Check if the main rectangle is clicked (fallback for dragging)
|
||||
if region.rect().contains(event.pos()):
|
||||
self.selected_region = region
|
||||
self.selected_handle = None
|
||||
@ -111,274 +169,209 @@ class OverlayCanvas(QWidget):
|
||||
return
|
||||
|
||||
if self.selected_handle is None:
|
||||
# Dragging the entire rectangle
|
||||
# Drag rectangle
|
||||
self.selected_region.x = event.x() - self.drag_offset.x()
|
||||
self.selected_region.y = event.y() - self.drag_offset.y()
|
||||
else:
|
||||
# Resizing the rectangle
|
||||
# Resize
|
||||
sr = self.selected_region
|
||||
if self.selected_handle == 0: # Top-left
|
||||
self.selected_region.w += self.selected_region.x - event.x()
|
||||
self.selected_region.h += self.selected_region.y - event.y()
|
||||
self.selected_region.x = event.x()
|
||||
self.selected_region.y = event.y()
|
||||
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: # Top-right
|
||||
self.selected_region.w = event.x() - self.selected_region.x
|
||||
self.selected_region.h += self.selected_region.y - event.y()
|
||||
self.selected_region.y = event.y()
|
||||
sr.w = event.x() - sr.x
|
||||
sr.h += sr.y - event.y()
|
||||
sr.y = event.y()
|
||||
elif self.selected_handle == 2: # Bottom-left
|
||||
self.selected_region.w += self.selected_region.x - event.x()
|
||||
self.selected_region.h = event.y() - self.selected_region.y
|
||||
self.selected_region.x = event.x()
|
||||
sr.w += sr.x - event.x()
|
||||
sr.h = event.y() - sr.y
|
||||
sr.x = event.x()
|
||||
elif self.selected_handle == 3: # Bottom-right
|
||||
self.selected_region.w = event.x() - self.selected_region.x
|
||||
self.selected_region.h = event.y() - self.selected_region.y
|
||||
sr.w = event.x() - sr.x
|
||||
sr.h = event.y() - sr.y
|
||||
|
||||
# Prevent negative width/height
|
||||
self.selected_region.w = max(self.selected_region.w, 10)
|
||||
self.selected_region.h = max(self.selected_region.h, 10)
|
||||
|
||||
self.update() # Trigger a repaint
|
||||
sr.w = max(sr.w, 10)
|
||||
sr.h = max(sr.h, 10)
|
||||
self.update()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if not self.edit_mode:
|
||||
return
|
||||
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.selected_region = None
|
||||
self.selected_handle = None # Deselect handle
|
||||
|
||||
self.selected_handle = None
|
||||
|
||||
class BorealisOverlay(QWidget):
|
||||
"""
|
||||
Project Borealis with Tesseract + Rich table + Rich progress bar,
|
||||
properly displayed with "with Progress(...) as progress".
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# Set window properties to cover the full screen
|
||||
# Fullscreen overlay
|
||||
screen_geometry = QApplication.primaryScreen().geometry()
|
||||
self.setGeometry(screen_geometry)
|
||||
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
||||
self.setAttribute(Qt.WA_TranslucentBackground, True) # Transparent background
|
||||
self.setAttribute(Qt.WA_TranslucentBackground, True)
|
||||
|
||||
# Create regions to draw and interact with, all regions have the same size and are moved higher up
|
||||
self.regions = [
|
||||
Region(250, 50, label="Experience"), # Moved slightly to the right
|
||||
Region(450, 50, label="Region 02"), # Moved slightly to the right
|
||||
Region(650, 50, label="Region 03") # Moved slightly to the right
|
||||
Region(250, 50, label="Experience"),
|
||||
]
|
||||
|
||||
# Create canvas and attach to window
|
||||
self.canvas = OverlayCanvas(self.regions, self)
|
||||
self.canvas.setGeometry(self.rect()) # Match the canvas size to the full window
|
||||
self.canvas.setGeometry(self.rect())
|
||||
|
||||
# Add title, Edit Mode UI, and buttons (Overlay Visibility checkbox)
|
||||
self.init_ui()
|
||||
# Tesseract
|
||||
self.engine = pytesseract
|
||||
|
||||
# Timer for polling OCR data
|
||||
self.points = []
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.collect_ocr_data)
|
||||
self.timer.start(1000) # Poll every second (default)
|
||||
|
||||
self.reader = None # Default OCR reader (None until an engine is selected)
|
||||
|
||||
def init_ui(self):
|
||||
"""Initialize UI components."""
|
||||
# Title label
|
||||
self.title_label = QLabel("Borealis Overlay", self)
|
||||
self.title_label.setStyleSheet("QLabel { color: white; font-size: 20px; font-weight: bold; }") # Adjusted title size
|
||||
self.title_label.move(10, 5)
|
||||
|
||||
# OCR Engine label and selection dropdown
|
||||
self.engine_label = QLabel("OCR Engine:", self)
|
||||
self.engine_label.setStyleSheet("QLabel { color: white; font-size: 14px; }")
|
||||
self.engine_label.move(10, 60) # Moved OCR Engine label slightly higher
|
||||
|
||||
self.engine_dropdown = QComboBox(self)
|
||||
self.engine_dropdown.setStyleSheet("""
|
||||
QComboBox {
|
||||
color: white;
|
||||
background-color: #2b2b2b;
|
||||
border: 1px solid #3c3f41;
|
||||
font-size: 14px;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
color: white;
|
||||
background-color: #2b2b2b;
|
||||
}
|
||||
""")
|
||||
self.engine_dropdown.addItem("Select OCR Engine") # Placeholder option
|
||||
self.engine_dropdown.addItem("Tesseract") # Only Tesseract for now
|
||||
self.engine_dropdown.addItem("EasyOCR") # Adding EasyOCR option
|
||||
self.engine_dropdown.move(100, 90) # Dropdown moved slightly up and aligned with label
|
||||
self.engine_dropdown.currentIndexChanged.connect(self.on_engine_selected)
|
||||
|
||||
# Polling rate dropdown
|
||||
self.polling_rate_label = QLabel("Polling Rate:", self)
|
||||
self.polling_rate_label.setStyleSheet("QLabel { color: white; font-size: 14px; }")
|
||||
self.polling_rate_label.move(10, 120)
|
||||
|
||||
self.polling_rate_dropdown = QComboBox(self)
|
||||
self.polling_rate_dropdown.setStyleSheet("""
|
||||
QComboBox {
|
||||
color: white;
|
||||
background-color: #2b2b2b;
|
||||
border: 1px solid #3c3f41;
|
||||
font-size: 14px;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
color: white;
|
||||
background-color: #2b2b2b;
|
||||
}
|
||||
""")
|
||||
self.polling_rate_dropdown.addItem("0.1 Seconds")
|
||||
self.polling_rate_dropdown.addItem("0.5 Seconds")
|
||||
self.polling_rate_dropdown.addItem("1 Second")
|
||||
self.polling_rate_dropdown.addItem("2 Seconds")
|
||||
self.polling_rate_dropdown.addItem("5 Seconds")
|
||||
self.polling_rate_dropdown.move(100, 150) # Dropdown moved slightly up
|
||||
self.polling_rate_dropdown.currentIndexChanged.connect(self.on_polling_rate_selected)
|
||||
|
||||
# Options label
|
||||
self.options_label = QLabel("Options", self)
|
||||
self.options_label.setStyleSheet("QLabel { color: white; font-size: 16px; font-weight: bold; }")
|
||||
self.options_label.move(10, 180) # Positioned above checkboxes
|
||||
|
||||
# Edit mode checkbox
|
||||
self.mode_toggle = QCheckBox("Edit Mode", self)
|
||||
self.mode_toggle.setStyleSheet("QCheckBox { color: white; }")
|
||||
self.mode_toggle.move(10, 210)
|
||||
self.mode_toggle.stateChanged.connect(self.toggle_edit_mode)
|
||||
|
||||
# Overlay Visibility checkbox
|
||||
self.visibility_checkbox = QCheckBox("Overlay Visibility", self)
|
||||
self.visibility_checkbox.setStyleSheet("QCheckBox { color: white; }")
|
||||
self.visibility_checkbox.move(10, 240) # Positioned below Edit Mode
|
||||
self.visibility_checkbox.setChecked(True) # Default to visible
|
||||
self.visibility_checkbox.stateChanged.connect(self.toggle_overlay_visibility)
|
||||
|
||||
# Collect Data checkbox for OCR functionality (disabled initially)
|
||||
self.collect_data_checkbox = QCheckBox("Collect Data", self)
|
||||
self.collect_data_checkbox.setStyleSheet("QCheckBox { color: white; }")
|
||||
self.collect_data_checkbox.move(10, 270) # Positioned below Overlay Visibility
|
||||
self.collect_data_checkbox.setEnabled(False) # Initially disabled
|
||||
self.collect_data_checkbox.stateChanged.connect(self.toggle_ocr)
|
||||
|
||||
# Data Collection Output label
|
||||
self.output_label = QLabel("Data Collection Output:", self)
|
||||
self.output_label.setStyleSheet("QLabel { color: white; font-size: 14px; font-weight: bold; }")
|
||||
self.output_label.move(10, 330) # Moved down by 50px
|
||||
|
||||
# Text area for OCR data display (with transparent background)
|
||||
self.ocr_display = QTextEdit(self)
|
||||
self.ocr_display.setStyleSheet("QTextEdit { color: white; background-color: transparent; font-size: 14px; }")
|
||||
self.ocr_display.setReadOnly(True)
|
||||
self.ocr_display.setGeometry(10, 360, 300, 400)
|
||||
|
||||
def on_engine_selected(self):
|
||||
"""Enable the Collect Data checkbox when an OCR engine is selected."""
|
||||
selected_engine = self.engine_dropdown.currentText()
|
||||
if selected_engine == "Tesseract":
|
||||
self.reader = pytesseract
|
||||
elif selected_engine == "EasyOCR":
|
||||
self.reader = easyocr.Reader(['en']) # Initialize EasyOCR reader for English language
|
||||
elif selected_engine == "Select OCR Engine":
|
||||
self.reader = None
|
||||
|
||||
if self.reader:
|
||||
self.collect_data_checkbox.setEnabled(True)
|
||||
else:
|
||||
self.collect_data_checkbox.setEnabled(False)
|
||||
|
||||
def on_polling_rate_selected(self):
|
||||
"""Update the polling rate based on dropdown selection."""
|
||||
polling_rate = self.polling_rate_dropdown.currentText()
|
||||
if polling_rate == "0.1 Seconds":
|
||||
self.timer.setInterval(100)
|
||||
elif polling_rate == "0.5 Seconds":
|
||||
self.timer.setInterval(500)
|
||||
elif polling_rate == "1 Second":
|
||||
self.timer.setInterval(1000)
|
||||
elif polling_rate == "2 Seconds":
|
||||
self.timer.setInterval(2000)
|
||||
elif polling_rate == "5 Seconds":
|
||||
self.timer.setInterval(5000)
|
||||
|
||||
def toggle_edit_mode(self, state):
|
||||
"""Enable or disable edit mode for dragging and resizing rectangles."""
|
||||
editing = (state == 2)
|
||||
self.canvas.edit_mode = editing # Pass the state to the canvas
|
||||
|
||||
def toggle_overlay_visibility(self):
|
||||
"""Toggle the visibility of the regions."""
|
||||
visible = self.visibility_checkbox.isChecked()
|
||||
for region in self.regions:
|
||||
region.visible = visible # Toggle visibility based on checkbox
|
||||
self.update() # Trigger a repaint
|
||||
|
||||
def toggle_ocr(self, state):
|
||||
"""Enable or disable OCR collection."""
|
||||
if state == Qt.Checked:
|
||||
self.collect_ocr_data()
|
||||
else:
|
||||
self.clear_ocr_data()
|
||||
self.timer.start(POLLING_RATE_MS)
|
||||
|
||||
def collect_ocr_data(self):
|
||||
"""Collect OCR data from each visible region."""
|
||||
if self.collect_data_checkbox.isChecked() and self.reader: # Only collect data if checkbox is checked
|
||||
for region in self.regions:
|
||||
if region.visible:
|
||||
# Capture the image of the region
|
||||
screenshot = ImageGrab.grab(bbox=(region.x, region.y, region.x + region.w, region.y + region.h))
|
||||
if not self.engine:
|
||||
return
|
||||
|
||||
# Preprocess the image (Convert to grayscale and apply threshold)
|
||||
processed_image = self.preprocess_image(screenshot)
|
||||
for region in self.regions:
|
||||
if region.visible:
|
||||
screenshot = ImageGrab.grab(
|
||||
bbox=(region.x, region.y, region.x + region.w, region.y + region.h)
|
||||
)
|
||||
processed_image = self.preprocess_image(screenshot)
|
||||
|
||||
# Convert the processed image to a numpy array for EasyOCR
|
||||
numpy_image = np.array(processed_image)
|
||||
text = pytesseract.image_to_string(processed_image, config='--psm 6 --oem 1')
|
||||
region.data = text.strip()
|
||||
|
||||
# Perform OCR on the preprocessed numpy image
|
||||
if self.reader == pytesseract:
|
||||
text = pytesseract.image_to_string(processed_image, config='--psm 6 --oem 1')
|
||||
if region.label.lower() == "experience":
|
||||
val = sanitize_experience_string(region.data)
|
||||
if val is not None:
|
||||
self.update_points(val)
|
||||
region.data = format_experience_value(val) + "%"
|
||||
else:
|
||||
# Get text from EasyOCR
|
||||
results = self.reader.readtext(numpy_image)
|
||||
region.data = "N/A"
|
||||
|
||||
if results:
|
||||
text = results[0][1] # If OCR detects text, use it
|
||||
else:
|
||||
text = "No text detected" # Fallback if no text is detected
|
||||
|
||||
region.data = text.strip()
|
||||
|
||||
self.display_ocr_data()
|
||||
self.display_ocr_data_in_terminal()
|
||||
|
||||
def preprocess_image(self, image):
|
||||
"""Preprocess the image to enhance text recognition."""
|
||||
# Convert image to grayscale
|
||||
gray_image = image.convert("L")
|
||||
gray = image.convert("L")
|
||||
scaled = gray.resize(
|
||||
(gray.width * 3, gray.height * 3),
|
||||
resample=Image.Resampling.LANCZOS
|
||||
)
|
||||
thresh = scaled.point(lambda p: p > 200 and 255)
|
||||
return thresh.filter(ImageFilter.MedianFilter(3))
|
||||
|
||||
# Apply threshold to make the image binary (black and white)
|
||||
threshold_image = gray_image.point(lambda p: p > 200 and 255)
|
||||
def update_points(self, new_val):
|
||||
now = time.time()
|
||||
if self.points:
|
||||
_, last_v = self.points[-1]
|
||||
if abs(new_val - last_v) < 1e-6:
|
||||
return
|
||||
if new_val < last_v:
|
||||
self.points.clear()
|
||||
self.points.append((now, new_val))
|
||||
if len(self.points) > MAX_DATA_POINTS:
|
||||
self.points.pop(0)
|
||||
|
||||
# Apply noise reduction (filter)
|
||||
processed_image = threshold_image.filter(ImageFilter.MedianFilter(3))
|
||||
return processed_image
|
||||
def compute_time_to_100(self):
|
||||
"""Return integer seconds until 100% or None if not feasible."""
|
||||
n = len(self.points)
|
||||
if n < 2:
|
||||
return None
|
||||
first_t, first_v = self.points[0]
|
||||
last_t, last_v = self.points[-1]
|
||||
diff_v = last_v - first_v
|
||||
if diff_v <= 0:
|
||||
return None
|
||||
|
||||
def clear_ocr_data(self):
|
||||
"""Clear the displayed OCR data."""
|
||||
self.ocr_display.clear()
|
||||
steps = n - 1
|
||||
total_time = last_t - first_t
|
||||
if total_time <= 0:
|
||||
return None
|
||||
|
||||
def display_ocr_data(self):
|
||||
"""Display OCR data in the text area on the left-hand side."""
|
||||
ocr_text = ""
|
||||
for region in self.regions:
|
||||
ocr_text += f"{region.label} Output:\n{region.data}\n\n" # Updated headers with "Output"
|
||||
self.ocr_display.setText(ocr_text)
|
||||
avg_change = diff_v / steps
|
||||
remain = 100.0 - last_v
|
||||
if remain <= 0:
|
||||
return None
|
||||
|
||||
avg_time = total_time / steps
|
||||
rate_per_s = avg_change / avg_time if avg_time > 0 else 0
|
||||
if rate_per_s <= 0:
|
||||
return None
|
||||
|
||||
return int(remain / rate_per_s)
|
||||
|
||||
def display_ocr_data_in_terminal(self):
|
||||
console = Console()
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
|
||||
console.print("[bold white]Project Borealis[/bold white]")
|
||||
console.print("[dim]Flyff Information Overlay[/dim]\n")
|
||||
|
||||
console.print(f"[bold]OCR Engine[/bold]: {OCR_ENGINE}")
|
||||
console.print(f"[bold]Data Polling Rate[/bold]: {POLLING_RATE_MS / 1000}s\n")
|
||||
|
||||
# Build the historical table
|
||||
table = Table(show_header=True, header_style=GREEN_HEADER_STYLE, style=None)
|
||||
table.add_column("Historical EXP", justify="center", style="green")
|
||||
table.add_column("Time Since Last Kill", justify="center", style="green")
|
||||
table.add_column("Average EXP Per Kill", justify="center", style="green")
|
||||
table.add_column("Average Time Between Kills", justify="center", style="green")
|
||||
|
||||
n = len(self.points)
|
||||
if n == 1:
|
||||
t0, v0 = self.points[0]
|
||||
exp_str = f"[green]{format_experience_value(v0)}%[/green]"
|
||||
table.add_row(exp_str, "N/A", "N/A", "N/A")
|
||||
else:
|
||||
for i in range(1, n):
|
||||
t_cur, v_cur = self.points[i]
|
||||
t_prev, v_prev = self.points[i - 1]
|
||||
|
||||
exp_str = f"[green]{format_experience_value(v_cur)}%[/green]"
|
||||
delta_t = t_cur - t_prev
|
||||
t_since_str = f"{delta_t:.1f}s"
|
||||
|
||||
diff_v = v_cur - self.points[0][1]
|
||||
steps = i
|
||||
avg_exp_str = f"{diff_v/steps:.4f}%"
|
||||
|
||||
total_time = t_cur - self.points[0][0]
|
||||
avg_kill_time = total_time / steps
|
||||
avg_time_str = f"{avg_kill_time:.1f}s"
|
||||
|
||||
table.add_row(exp_str, t_since_str, avg_exp_str, avg_time_str)
|
||||
|
||||
console.print(table)
|
||||
console.print()
|
||||
|
||||
# Prepare the progress bar
|
||||
current_exp = self.points[-1][1] if self.points else 0.0
|
||||
secs_left = self.compute_time_to_100()
|
||||
time_str = format_duration(secs_left) if secs_left is not None else "???"
|
||||
|
||||
# We'll display a single progress bar line in a context manager
|
||||
# Setting transient=False ensures it remains in the console
|
||||
with Progress(
|
||||
TextColumn("[bold white]Predicted Time to Level:[/bold white] "),
|
||||
BarColumn(bar_width=30),
|
||||
TextColumn(" [green]{task.percentage:>5.2f}%[/green] "),
|
||||
TextColumn(f"[orange]{time_str}[/orange] until 100%", justify="right"),
|
||||
console=console,
|
||||
transient=False,
|
||||
) as progress:
|
||||
task_id = progress.add_task("", total=100, completed=current_exp)
|
||||
# Force a manual refresh so the bar appears immediately
|
||||
progress.refresh()
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
window = BorealisOverlay()
|
||||
window.setWindowTitle("Borealis Overlay") # Set application window title
|
||||
window.show() # Explicitly show the window
|
||||
window.setWindowTitle("Project Borealis Overlay (Fixed Progress Bar)")
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user