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("", self._on_left_click) self.grid_canvas.bind("", self._on_left_drag) self.grid_canvas.bind("", self._on_right_click) self.grid_canvas.bind("", 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("", lambda _event: self.prev_image()) self.root.bind("", 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()