365 lines
12 KiB
Python
365 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
import tkinter as tk
|
|
from tkinter import messagebox
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
|
|
IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".tif", ".tiff", ".bmp", ".webp"}
|
|
GRID_SIZE = 17
|
|
CELL_SIZE = 28
|
|
PREVIEW_SIZE = 560
|
|
|
|
|
|
def read_image(path: Path) -> np.ndarray:
|
|
raw = np.fromfile(str(path), dtype=np.uint8)
|
|
image = cv2.imdecode(raw, cv2.IMREAD_COLOR)
|
|
if image is None:
|
|
raise ValueError(f"Cannot read image: {path}")
|
|
return image
|
|
|
|
|
|
def write_image(path: Path, image: np.ndarray) -> None:
|
|
ok, encoded = cv2.imencode(path.suffix, image)
|
|
if not ok:
|
|
raise ValueError(f"Cannot encode image: {path}")
|
|
encoded.tofile(str(path))
|
|
|
|
|
|
def matrix_to_text(matrix: np.ndarray) -> str:
|
|
lines = []
|
|
for row in matrix:
|
|
lines.append("".join("1" if value else "0" for value in row))
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def text_to_matrix(path: Path) -> np.ndarray:
|
|
matrix = np.zeros((GRID_SIZE, GRID_SIZE), dtype=np.uint8)
|
|
if not path.exists():
|
|
return matrix
|
|
|
|
rows = []
|
|
for line in path.read_text(encoding="utf-8").splitlines():
|
|
stripped = line.strip()
|
|
if not stripped or stripped.startswith("#"):
|
|
continue
|
|
rows.append(stripped)
|
|
if len(rows) == GRID_SIZE:
|
|
break
|
|
|
|
for row_index, row_text in enumerate(rows[:GRID_SIZE]):
|
|
for col_index, char in enumerate(row_text[:GRID_SIZE]):
|
|
matrix[row_index, col_index] = 1 if char == "1" else 0
|
|
|
|
return matrix
|
|
|
|
|
|
def matrix_to_png(matrix: np.ndarray, scale: int = 24) -> np.ndarray:
|
|
image = np.where(matrix > 0, 0, 255).astype(np.uint8)
|
|
return cv2.resize(
|
|
image,
|
|
(GRID_SIZE * scale, GRID_SIZE * scale),
|
|
interpolation=cv2.INTER_NEAREST,
|
|
)
|
|
|
|
|
|
class MatrixEditor:
|
|
def __init__(
|
|
self,
|
|
root: tk.Tk,
|
|
image_paths: list[Path],
|
|
output_dir: Path,
|
|
load_generated_dir: Path | None,
|
|
) -> None:
|
|
self.root = root
|
|
self.image_paths = image_paths
|
|
self.output_dir = output_dir
|
|
self.load_generated_dir = load_generated_dir
|
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.index = 0
|
|
self.matrix = np.zeros((GRID_SIZE, GRID_SIZE), dtype=np.uint8)
|
|
self.paint_value = 1
|
|
self.preview_photo: tk.PhotoImage | None = None
|
|
self.preview_temp = Path("/private/tmp/matrix17_editor_preview.png")
|
|
|
|
self.root.title("17x17 Matrix Editor")
|
|
self._build_ui()
|
|
self._bind_keys()
|
|
self.load_current_image()
|
|
|
|
@property
|
|
def image_path(self) -> Path:
|
|
return self.image_paths[self.index]
|
|
|
|
@property
|
|
def answer_path(self) -> Path:
|
|
return self.output_dir / f"{self.image_path.stem}_matrix17.txt"
|
|
|
|
@property
|
|
def answer_png_path(self) -> Path:
|
|
return self.output_dir / f"{self.image_path.stem}_matrix17.png"
|
|
|
|
def _build_ui(self) -> None:
|
|
main = tk.Frame(self.root)
|
|
main.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
|
|
left = tk.Frame(main)
|
|
left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
right = tk.Frame(main)
|
|
right.pack(side=tk.LEFT, fill=tk.Y, padx=(12, 0))
|
|
|
|
self.preview_canvas = tk.Canvas(
|
|
left,
|
|
width=PREVIEW_SIZE,
|
|
height=PREVIEW_SIZE,
|
|
bg="white",
|
|
highlightthickness=1,
|
|
highlightbackground="#999",
|
|
)
|
|
self.preview_canvas.pack(fill=tk.BOTH, expand=True)
|
|
|
|
self.grid_canvas = tk.Canvas(
|
|
right,
|
|
width=GRID_SIZE * CELL_SIZE + 1,
|
|
height=GRID_SIZE * CELL_SIZE + 1,
|
|
bg="white",
|
|
highlightthickness=1,
|
|
highlightbackground="#777",
|
|
)
|
|
self.grid_canvas.pack()
|
|
self.grid_canvas.bind("<Button-1>", self._on_left_click)
|
|
self.grid_canvas.bind("<B1-Motion>", self._on_left_drag)
|
|
self.grid_canvas.bind("<Button-3>", self._on_right_click)
|
|
self.grid_canvas.bind("<B3-Motion>", self._on_right_drag)
|
|
|
|
buttons = tk.Frame(right)
|
|
buttons.pack(fill=tk.X, pady=(10, 0))
|
|
|
|
tk.Button(buttons, text="上一张", command=self.prev_image).grid(
|
|
row=0, column=0, sticky="ew", padx=2, pady=2
|
|
)
|
|
tk.Button(buttons, text="下一张", command=self.next_image).grid(
|
|
row=0, column=1, sticky="ew", padx=2, pady=2
|
|
)
|
|
tk.Button(buttons, text="保存", command=self.save_answer).grid(
|
|
row=1, column=0, sticky="ew", padx=2, pady=2
|
|
)
|
|
tk.Button(buttons, text="清空", command=self.clear_matrix).grid(
|
|
row=1, column=1, sticky="ew", padx=2, pady=2
|
|
)
|
|
tk.Button(buttons, text="载入生成结果", command=self.load_generated).grid(
|
|
row=2, column=0, columnspan=2, sticky="ew", padx=2, pady=2
|
|
)
|
|
|
|
buttons.columnconfigure(0, weight=1)
|
|
buttons.columnconfigure(1, weight=1)
|
|
|
|
self.status = tk.Label(right, text="", anchor="w", justify="left")
|
|
self.status.pack(fill=tk.X, pady=(8, 0))
|
|
|
|
def _bind_keys(self) -> None:
|
|
self.root.bind("<Left>", lambda _event: self.prev_image())
|
|
self.root.bind("<Right>", lambda _event: self.next_image())
|
|
self.root.bind("s", lambda _event: self.save_answer())
|
|
self.root.bind("S", lambda _event: self.save_answer())
|
|
self.root.bind("c", lambda _event: self.clear_matrix())
|
|
self.root.bind("C", lambda _event: self.clear_matrix())
|
|
self.root.bind("1", lambda _event: self.set_paint_value(1))
|
|
self.root.bind("0", lambda _event: self.set_paint_value(0))
|
|
|
|
def set_paint_value(self, value: int) -> None:
|
|
self.paint_value = 1 if value else 0
|
|
self._update_status()
|
|
|
|
def load_current_image(self) -> None:
|
|
self.matrix = text_to_matrix(self.answer_path)
|
|
self._draw_preview()
|
|
self._draw_grid()
|
|
self._update_status()
|
|
|
|
def _draw_preview(self) -> None:
|
|
image = read_image(self.image_path)
|
|
height, width = image.shape[:2]
|
|
scale = min(PREVIEW_SIZE / height, PREVIEW_SIZE / width)
|
|
new_width = max(1, int(round(width * scale)))
|
|
new_height = max(1, int(round(height * scale)))
|
|
resized = cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_NEAREST)
|
|
|
|
canvas = np.full((PREVIEW_SIZE, PREVIEW_SIZE, 3), 255, dtype=np.uint8)
|
|
x = (PREVIEW_SIZE - new_width) // 2
|
|
y = (PREVIEW_SIZE - new_height) // 2
|
|
canvas[y : y + new_height, x : x + new_width] = resized
|
|
write_image(self.preview_temp, canvas)
|
|
self.preview_photo = tk.PhotoImage(file=str(self.preview_temp))
|
|
self.preview_canvas.delete("all")
|
|
self.preview_canvas.create_image(0, 0, anchor="nw", image=self.preview_photo)
|
|
|
|
def _draw_grid(self) -> None:
|
|
self.grid_canvas.delete("all")
|
|
for row in range(GRID_SIZE):
|
|
for col in range(GRID_SIZE):
|
|
x1 = col * CELL_SIZE
|
|
y1 = row * CELL_SIZE
|
|
x2 = x1 + CELL_SIZE
|
|
y2 = y1 + CELL_SIZE
|
|
fill = "black" if self.matrix[row, col] else "white"
|
|
self.grid_canvas.create_rectangle(
|
|
x1,
|
|
y1,
|
|
x2,
|
|
y2,
|
|
fill=fill,
|
|
outline="#888",
|
|
)
|
|
|
|
for index in range(GRID_SIZE + 1):
|
|
pos = index * CELL_SIZE
|
|
width = 2 if index in (0, GRID_SIZE) else 1
|
|
self.grid_canvas.create_line(0, pos, GRID_SIZE * CELL_SIZE, pos, fill="#555", width=width)
|
|
self.grid_canvas.create_line(pos, 0, pos, GRID_SIZE * CELL_SIZE, fill="#555", width=width)
|
|
|
|
def _grid_cell_from_event(self, event: tk.Event) -> tuple[int, int] | None:
|
|
col = int(event.x // CELL_SIZE)
|
|
row = int(event.y // CELL_SIZE)
|
|
if 0 <= row < GRID_SIZE and 0 <= col < GRID_SIZE:
|
|
return row, col
|
|
return None
|
|
|
|
def _set_cell_from_event(self, event: tk.Event, value: int) -> None:
|
|
cell = self._grid_cell_from_event(event)
|
|
if cell is None:
|
|
return
|
|
row, col = cell
|
|
if self.matrix[row, col] == value:
|
|
return
|
|
self.matrix[row, col] = value
|
|
self._draw_grid()
|
|
self._update_status()
|
|
|
|
def _on_left_click(self, event: tk.Event) -> None:
|
|
cell = self._grid_cell_from_event(event)
|
|
if cell is None:
|
|
return
|
|
row, col = cell
|
|
self.paint_value = 0 if self.matrix[row, col] else 1
|
|
self.matrix[row, col] = self.paint_value
|
|
self._draw_grid()
|
|
self._update_status()
|
|
|
|
def _on_left_drag(self, event: tk.Event) -> None:
|
|
self._set_cell_from_event(event, self.paint_value)
|
|
|
|
def _on_right_click(self, event: tk.Event) -> None:
|
|
self.paint_value = 0
|
|
self._set_cell_from_event(event, 0)
|
|
self._update_status()
|
|
|
|
def _on_right_drag(self, event: tk.Event) -> None:
|
|
self._set_cell_from_event(event, 0)
|
|
|
|
def save_answer(self) -> None:
|
|
self.answer_path.write_text(matrix_to_text(self.matrix), encoding="utf-8")
|
|
write_image(self.answer_png_path, matrix_to_png(self.matrix))
|
|
self._update_status(saved=True)
|
|
|
|
def clear_matrix(self) -> None:
|
|
self.matrix[:, :] = 0
|
|
self._draw_grid()
|
|
self._update_status()
|
|
|
|
def load_generated(self) -> None:
|
|
if self.load_generated_dir is None:
|
|
messagebox.showinfo("提示", "未配置生成结果目录")
|
|
return
|
|
|
|
path = self.load_generated_dir / f"{self.image_path.stem}_matrix17.txt"
|
|
if not path.exists():
|
|
messagebox.showinfo("提示", f"找不到生成结果:\n{path}")
|
|
return
|
|
|
|
self.matrix = text_to_matrix(path)
|
|
self._draw_grid()
|
|
self._update_status()
|
|
|
|
def prev_image(self) -> None:
|
|
self.save_answer()
|
|
self.index = (self.index - 1) % len(self.image_paths)
|
|
self.load_current_image()
|
|
|
|
def next_image(self) -> None:
|
|
self.save_answer()
|
|
self.index = (self.index + 1) % len(self.image_paths)
|
|
self.load_current_image()
|
|
|
|
def _update_status(self, saved: bool = False) -> None:
|
|
filled = int(self.matrix.sum())
|
|
two_by_two = count_2x2_blocks(self.matrix)
|
|
saved_text = "已保存\n" if saved else ""
|
|
self.status.config(
|
|
text=(
|
|
f"{saved_text}"
|
|
f"{self.index + 1}/{len(self.image_paths)}\n"
|
|
f"{self.image_path.name}\n"
|
|
f"黑格:{filled}\n"
|
|
f"2x2 全黑:{two_by_two}\n"
|
|
f"左键:切换/拖拽绘制\n"
|
|
f"右键:擦除\n"
|
|
f"S 保存,←/→ 切换"
|
|
)
|
|
)
|
|
|
|
|
|
def count_2x2_blocks(matrix: np.ndarray) -> int:
|
|
image = matrix > 0
|
|
total = 0
|
|
for row in range(GRID_SIZE - 1):
|
|
for col in range(GRID_SIZE - 1):
|
|
total += int(image[row : row + 2, col : col + 2].sum() == 4)
|
|
return total
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Draw manual 17x17 matrix labels.")
|
|
parser.add_argument(
|
|
"--input-dir",
|
|
type=Path,
|
|
default=Path("PreProcessing/original_characters"),
|
|
)
|
|
parser.add_argument(
|
|
"--output-dir",
|
|
type=Path,
|
|
default=Path("PreProcessing/manual_matrices"),
|
|
)
|
|
parser.add_argument(
|
|
"--load-generated-dir",
|
|
type=Path,
|
|
default=Path("PreProcessing/processed_characters"),
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
image_paths = sorted(
|
|
path
|
|
for path in args.input_dir.iterdir()
|
|
if path.is_file() and path.suffix.lower() in IMAGE_SUFFIXES
|
|
)
|
|
if not image_paths:
|
|
raise SystemExit(f"No images found in {args.input_dir}")
|
|
|
|
root = tk.Tk()
|
|
MatrixEditor(
|
|
root=root,
|
|
image_paths=image_paths,
|
|
output_dir=args.output_dir,
|
|
load_generated_dir=args.load_generated_dir,
|
|
)
|
|
root.mainloop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|