oneocr / _archive /region_selector.py
OneOCR Dev
OneOCR - reverse engineering complete, ONNX pipeline 53% match rate
ce847d4
"""Region Selector V2 — fullscreen overlay with multi-region editing.
Features:
- Show existing regions (numbered, with coordinates)
- Draw new regions by click-and-drag
- Edit existing regions: drag to move, resize handles on corners/edges
- Number badges centered on each region
- Coordinate labels on sides
- ESC = cancel, Enter/double-click = confirm all, Right-click = delete hovered
Flow:
1. Take a screenshot of the entire primary monitor (mss).
2. Show a fullscreen, borderless, topmost tkinter window with the screenshot.
3. Display existing regions from passed list (numbered, colored).
4. User can draw new regions, drag-move existing, resize via handles.
5. Return all regions as list on confirm, or None on cancel.
Runs in a separate thread to avoid tkinter ↔ Qt conflicts.
"""
from __future__ import annotations
import threading
from src.utils.logger import logger
def select_region_on_screen(
existing_regions: list[dict] | None = None,
auto_confirm: bool = False,
) -> list[dict] | None:
"""Open fullscreen overlay for region selection/editing.
Args:
existing_regions: List of dicts with {x, y, width, height} to show.
These are editable and will be included in the return value.
auto_confirm: If True, automatically confirm after drawing a single
new region (no Enter/double-click needed). Used by Quick OCR.
Returns:
List of {x, y, width, height} dicts (all regions), or None if cancelled.
"""
result: list[dict] | None = None
error: Exception | None = None
done_event = threading.Event()
if existing_regions is None:
existing_regions = []
_auto_confirm = auto_confirm
def _run_selector() -> None:
nonlocal result, error
try:
import tkinter as tk
import tkinter.font as tkfont
import mss
# ── Colors for up to 8 regions ──
COLORS = [
"#00ff88", "#ff6b6b", "#4ecdc4", "#ffe66d",
"#a8e6cf", "#ff8a5c", "#88d8f7", "#d4a5ff",
]
HANDLE_SIZE = 8
# ── 1. Get screen dimensions ──
with mss.mss() as sct:
monitor = sct.monitors[1]
scr_w, scr_h = monitor["width"], monitor["height"]
# ── 2. Tkinter window ──
root = tk.Tk()
root.title("Region Selector")
root.attributes("-fullscreen", True)
root.attributes("-topmost", True)
root.configure(cursor="crosshair")
root.overrideredirect(True)
# Semi-transparent overlay — game visible underneath
root.attributes("-alpha", 0.35)
canvas = tk.Canvas(root, width=scr_w, height=scr_h,
highlightthickness=0, cursor="crosshair",
bg="#0d0d1a")
canvas.pack(fill=tk.BOTH, expand=True)
# ── Region storage ──
# Each region: {"x": int, "y": int, "width": int, "height": int}
regions: list[dict] = [dict(r) for r in existing_regions]
# Canvas item IDs for each region: {index: {rect, label, coords, handles[]}}
region_items: dict[int, dict] = {}
# ── State ──
drawing = False
draw_start_x = draw_start_y = 0
draw_rect_id = None
dragging_idx: int | None = None
dragging_handle: str | None = None # "move", "nw", "ne", "sw", "se", "n", "s", "e", "w"
drag_offset_x = drag_offset_y = 0
drag_orig: dict | None = None
bold_font = tkfont.Font(family="Segoe UI", size=16, weight="bold")
coord_font = tkfont.Font(family="Consolas", size=10)
info_font = tkfont.Font(family="Segoe UI", size=13, weight="bold")
# ── Instructions ──
INSTRUCTIONS_PL = "Przeciągnij = Nowy Region | Przesuń Istniejący | Narożniki = Zmień Rozmiar | PPM = Usuń | Enter = Zatwierdź | ESC = Anuluj"
INSTRUCTIONS_EN = "Drag = New Region | Move Existing | Corners = Resize | RMB = Delete | Enter = Confirm | ESC = Cancel"
canvas.create_text(
scr_w // 2, 28,
text=INSTRUCTIONS_PL,
fill="#ffffff", font=info_font,
)
canvas.create_text(
scr_w // 2, 52,
text=INSTRUCTIONS_EN,
fill="#aaaaaa", font=coord_font,
)
# ── Draw Helpers ──
def _color(idx: int) -> str:
return COLORS[idx % len(COLORS)]
def _draw_region(idx: int) -> None:
"""Draw/redraw a single region with label, coords, handles."""
r = regions[idx]
x, y, w, h = r["x"], r["y"], r["width"], r["height"]
color = _color(idx)
# Remove old items if re-drawing
if idx in region_items:
for key, item_id in region_items[idx].items():
if key == "handles":
for hid in item_id:
canvas.delete(hid)
else:
canvas.delete(item_id)
# Rectangle
rect_id = canvas.create_rectangle(
x, y, x + w, y + h,
outline=color, width=2,
)
# Number badge in center
cx, cy = x + w // 2, y + h // 2
label_id = canvas.create_text(
cx, cy,
text=str(idx + 1),
fill=color, font=bold_font,
)
# Coordinate labels on sides
coord_text = f"{x},{y} {w}×{h}"
coord_id = canvas.create_text(
x + w // 2, y - 12,
text=coord_text,
fill=color, font=coord_font,
)
# Resize handles (8 positions)
handles = []
handle_positions = {
"nw": (x, y),
"ne": (x + w, y),
"sw": (x, y + h),
"se": (x + w, y + h),
"n": (x + w // 2, y),
"s": (x + w // 2, y + h),
"w": (x, y + h // 2),
"e": (x + w, y + h // 2),
}
for hname, (hx, hy) in handle_positions.items():
hid = canvas.create_rectangle(
hx - HANDLE_SIZE, hy - HANDLE_SIZE,
hx + HANDLE_SIZE, hy + HANDLE_SIZE,
fill=color, outline="white", width=1,
tags=(f"handle_{idx}_{hname}",),
)
handles.append(hid)
region_items[idx] = {
"rect": rect_id,
"label": label_id,
"coords": coord_id,
"handles": handles,
}
def _redraw_all() -> None:
"""Redraw all regions."""
# Clear old items for removed regions
for idx in list(region_items.keys()):
if idx >= len(regions):
for key, item_id in region_items[idx].items():
if key == "handles":
for hid in item_id:
canvas.delete(hid)
else:
canvas.delete(item_id)
del region_items[idx]
for i in range(len(regions)):
_draw_region(i)
def _hit_test(mx: int, my: int) -> tuple[int | None, str]:
"""Find which region/handle is under mouse cursor.
Returns (region_index, handle_name) or (None, '').
"""
# Check handles first (higher priority)
for idx in range(len(regions) - 1, -1, -1):
r = regions[idx]
x, y, w, h = r["x"], r["y"], r["width"], r["height"]
handle_positions = {
"nw": (x, y),
"ne": (x + w, y),
"sw": (x, y + h),
"se": (x + w, y + h),
"n": (x + w // 2, y),
"s": (x + w // 2, y + h),
"w": (x, y + h // 2),
"e": (x + w, y + h // 2),
}
for hname, (hx, hy) in handle_positions.items():
if abs(mx - hx) <= HANDLE_SIZE + 2 and abs(my - hy) <= HANDLE_SIZE + 2:
return idx, hname
# Check region body (for move)
for idx in range(len(regions) - 1, -1, -1):
r = regions[idx]
if (r["x"] <= mx <= r["x"] + r["width"] and
r["y"] <= my <= r["y"] + r["height"]):
return idx, "move"
return None, ""
# ── Initial draw ──
_redraw_all()
# ── Event Handlers ──
def on_press(event: tk.Event) -> None:
nonlocal drawing, draw_start_x, draw_start_y, draw_rect_id
nonlocal dragging_idx, dragging_handle, drag_offset_x, drag_offset_y, drag_orig
idx, handle = _hit_test(event.x, event.y)
if idx is not None:
# Start dragging existing region
dragging_idx = idx
dragging_handle = handle
r = regions[idx]
drag_offset_x = event.x - r["x"]
drag_offset_y = event.y - r["y"]
drag_orig = dict(r)
drawing = False
else:
# Start drawing new region
drawing = True
draw_start_x, draw_start_y = event.x, event.y
draw_rect_id = canvas.create_rectangle(
event.x, event.y, event.x, event.y,
outline="#00ffaa", width=3, dash=(8, 4),
)
dragging_idx = None
def on_drag(event: tk.Event) -> None:
nonlocal draw_rect_id
if drawing and draw_rect_id is not None:
canvas.coords(draw_rect_id, draw_start_x, draw_start_y,
event.x, event.y)
return
if dragging_idx is not None and drag_orig is not None:
r = regions[dragging_idx]
ox, oy = drag_orig["x"], drag_orig["y"]
ow, oh = drag_orig["width"], drag_orig["height"]
if dragging_handle == "move":
r["x"] = event.x - drag_offset_x
r["y"] = event.y - drag_offset_y
elif dragging_handle == "se":
r["width"] = max(20, event.x - ox)
r["height"] = max(20, event.y - oy)
elif dragging_handle == "nw":
r["x"] = min(event.x, ox + ow - 20)
r["y"] = min(event.y, oy + oh - 20)
r["width"] = ox + ow - r["x"]
r["height"] = oy + oh - r["y"]
elif dragging_handle == "ne":
r["y"] = min(event.y, oy + oh - 20)
r["width"] = max(20, event.x - ox)
r["height"] = oy + oh - r["y"]
elif dragging_handle == "sw":
r["x"] = min(event.x, ox + ow - 20)
r["width"] = ox + ow - r["x"]
r["height"] = max(20, event.y - oy)
elif dragging_handle == "n":
r["y"] = min(event.y, oy + oh - 20)
r["height"] = oy + oh - r["y"]
elif dragging_handle == "s":
r["height"] = max(20, event.y - oy)
elif dragging_handle == "w":
r["x"] = min(event.x, ox + ow - 20)
r["width"] = ox + ow - r["x"]
elif dragging_handle == "e":
r["width"] = max(20, event.x - ox)
_draw_region(dragging_idx)
def on_release(event: tk.Event) -> None:
nonlocal drawing, draw_rect_id, dragging_idx, drag_orig
if drawing and draw_rect_id is not None:
canvas.delete(draw_rect_id)
draw_rect_id = None
ex, ey = event.x, event.y
x1, y1 = min(draw_start_x, ex), min(draw_start_y, ey)
x2, y2 = max(draw_start_x, ex), max(draw_start_y, ey)
w, h = x2 - x1, y2 - y1
if w >= 10 and h >= 10:
regions.append({"x": x1, "y": y1, "width": w, "height": h})
_redraw_all()
# Auto-confirm: if exactly 1 region drawn and auto_confirm enabled
if _auto_confirm and len(regions) == 1:
root.after(120, on_confirm)
drawing = False
dragging_idx = None
drag_orig = None
def on_right_click(event: tk.Event) -> None:
"""Right-click = delete hovered region."""
idx, _ = _hit_test(event.x, event.y)
if idx is not None:
regions.pop(idx)
_redraw_all()
def on_confirm(_event: tk.Event = None) -> None:
nonlocal result
if regions:
result = regions
else:
result = None
root.destroy()
def on_cancel(_event: tk.Event = None) -> None:
nonlocal result
result = None
root.destroy()
def on_double_click(_event: tk.Event) -> None:
"""Double-click = confirm."""
on_confirm()
canvas.bind("<ButtonPress-1>", on_press)
canvas.bind("<B1-Motion>", on_drag)
canvas.bind("<ButtonRelease-1>", on_release)
canvas.bind("<ButtonPress-3>", on_right_click)
canvas.bind("<Double-Button-1>", on_double_click)
root.bind("<Return>", on_confirm)
root.bind("<Escape>", on_cancel)
root.mainloop()
except Exception as exc:
error = exc
logger.error("Region selector error: {}", exc)
finally:
done_event.set()
# Run tkinter in its own thread
t = threading.Thread(target=_run_selector, daemon=True, name="region-selector")
t.start()
done_event.wait(timeout=120)
if error:
raise error
return result