new file: PreProcessing/.gitignore
new file: PreProcessing/README.md new file: PreProcessing/scripts/compare_manual_matrix17.py new file: PreProcessing/scripts/draw_matrix17.py modified: PreProcessing/scripts/process_characters.py new file: PreProcessing/scripts/run_preprocessing.py
14
PreProcessing/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Generated preprocessing outputs
|
||||
comparisons/
|
||||
manual_diffs/
|
||||
manual_matrices/
|
||||
original_characters/
|
||||
processed_characters/
|
||||
svg_characters/
|
||||
out_cleaned/
|
||||
out_matrix/
|
||||
|
||||
# Local caches
|
||||
.matplotlib-cache/
|
||||
scripts/__pycache__/
|
||||
*.pyc
|
||||
49
PreProcessing/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# PreProcessing 预处理
|
||||
|
||||
这个目录包含17*17型特殊种类篆文字图像的预处理流程。日常批量处理请使用 `scripts/run_preprocessing.py`。
|
||||
|
||||
## 脚本用法
|
||||
|
||||
依赖见 `scripts/requirements.txt`。如果环境里还没有安装,可以先运行:
|
||||
|
||||
```bash
|
||||
.venv/bin/pip install -r PreProcessing/scripts/requirements.txt
|
||||
```
|
||||
|
||||
从项目根目录运行:
|
||||
|
||||
```bash
|
||||
.venv/bin/python PreProcessing/scripts/run_preprocessing.py \
|
||||
--input-dir PreProcessing/original_characters \
|
||||
--cleaned-dir PreProcessing/out_cleaned \
|
||||
--matrix-dir PreProcessing/out_matrix
|
||||
```
|
||||
|
||||
参数含义:
|
||||
|
||||
- `--input-dir`:原始图像输入文件夹
|
||||
- `--cleaned-dir`:cleaned 二值图输出文件夹
|
||||
- `--matrix-dir`:17x17 矩阵文本输出文件夹
|
||||
|
||||
输出命名会和输入文件名对齐:
|
||||
|
||||
- cleaned 图像:`原文件名_cleaned.png`
|
||||
- 矩阵文本:`原文件名_matrix.txt`
|
||||
|
||||
矩阵文本是纯 17 行,每行 17 个字符,只包含 `0` 和 `1`。
|
||||
|
||||
## 可选调参
|
||||
|
||||
默认参数会输出 17x17 矩阵。必要时可以追加这些参数:
|
||||
|
||||
- `--grid-size`,默认 `17`
|
||||
- `--cell-samples`,默认 `32`
|
||||
- `--open-kernel-size`
|
||||
- `--close-kernel-size`
|
||||
- `--median-size`
|
||||
- `--min-component-area`
|
||||
- `--triangle-threshold`
|
||||
|
||||
## 目录说明
|
||||
|
||||
`comparisons/`、`manual_diffs/`、`manual_matrices/`、`processed_characters/`、`svg_characters/` 等测试和比较用目录已经在 `PreProcessing/.gitignore` 中忽略。
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 7.2 KiB |
237
PreProcessing/scripts/compare_manual_matrix17.py
Normal file
@@ -0,0 +1,237 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
GRID_SIZE = 17
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MatrixMetrics:
|
||||
stem: str
|
||||
true_positive: int
|
||||
false_positive: int
|
||||
false_negative: int
|
||||
manual_cells: int
|
||||
generated_cells: int
|
||||
manual_2x2: int
|
||||
generated_2x2: int
|
||||
|
||||
@property
|
||||
def precision(self) -> float:
|
||||
denominator = self.true_positive + self.false_positive
|
||||
return self.true_positive / denominator if denominator else 1.0
|
||||
|
||||
@property
|
||||
def recall(self) -> float:
|
||||
denominator = self.true_positive + self.false_negative
|
||||
return self.true_positive / denominator if denominator else 1.0
|
||||
|
||||
@property
|
||||
def f1(self) -> float:
|
||||
denominator = self.precision + self.recall
|
||||
return 2.0 * self.precision * self.recall / denominator if denominator else 0.0
|
||||
|
||||
|
||||
def read_matrix(path: Path) -> np.ndarray:
|
||||
rows: list[str] = []
|
||||
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
|
||||
|
||||
if len(rows) != GRID_SIZE:
|
||||
raise ValueError(f"{path} has {len(rows)} matrix rows, expected {GRID_SIZE}")
|
||||
|
||||
matrix = np.zeros((GRID_SIZE, GRID_SIZE), dtype=np.uint8)
|
||||
for row_index, row_text in enumerate(rows):
|
||||
if len(row_text) < GRID_SIZE:
|
||||
raise ValueError(
|
||||
f"{path} row {row_index + 1} has {len(row_text)} columns, "
|
||||
f"expected {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 count_2x2_blocks(matrix: np.ndarray) -> int:
|
||||
total = 0
|
||||
for row in range(GRID_SIZE - 1):
|
||||
for col in range(GRID_SIZE - 1):
|
||||
total += int(matrix[row : row + 2, col : col + 2].sum() == 4)
|
||||
return total
|
||||
|
||||
|
||||
def diff_image(manual: np.ndarray, generated: np.ndarray, scale: int) -> np.ndarray:
|
||||
both = (manual == 1) & (generated == 1)
|
||||
false_positive = (manual == 0) & (generated == 1)
|
||||
false_negative = (manual == 1) & (generated == 0)
|
||||
|
||||
image = np.full((GRID_SIZE, GRID_SIZE, 3), 255, dtype=np.uint8)
|
||||
image[both] = (0, 0, 0)
|
||||
image[false_positive] = (40, 40, 220)
|
||||
image[false_negative] = (220, 80, 40)
|
||||
return cv2.resize(image, (GRID_SIZE * scale, GRID_SIZE * scale), interpolation=cv2.INTER_NEAREST)
|
||||
|
||||
|
||||
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 metrics_for_pair(manual_path: Path, generated_path: Path) -> MatrixMetrics:
|
||||
manual = read_matrix(manual_path)
|
||||
generated = read_matrix(generated_path)
|
||||
true_positive = int(((manual == 1) & (generated == 1)).sum())
|
||||
false_positive = int(((manual == 0) & (generated == 1)).sum())
|
||||
false_negative = int(((manual == 1) & (generated == 0)).sum())
|
||||
stem = manual_path.name.removesuffix("_matrix17.txt")
|
||||
return MatrixMetrics(
|
||||
stem=stem,
|
||||
true_positive=true_positive,
|
||||
false_positive=false_positive,
|
||||
false_negative=false_negative,
|
||||
manual_cells=int(manual.sum()),
|
||||
generated_cells=int(generated.sum()),
|
||||
manual_2x2=count_2x2_blocks(manual),
|
||||
generated_2x2=count_2x2_blocks(generated),
|
||||
)
|
||||
|
||||
|
||||
def print_table(metrics: list[MatrixMetrics]) -> None:
|
||||
header = (
|
||||
"stem",
|
||||
"P",
|
||||
"R",
|
||||
"F1",
|
||||
"TP",
|
||||
"FP",
|
||||
"FN",
|
||||
"manual",
|
||||
"gen",
|
||||
"manual_2x2",
|
||||
"gen_2x2",
|
||||
)
|
||||
print(
|
||||
f"{header[0]:42} {header[1]:>6} {header[2]:>6} {header[3]:>6} "
|
||||
f"{header[4]:>4} {header[5]:>4} {header[6]:>4} {header[7]:>6} "
|
||||
f"{header[8]:>6} {header[9]:>11} {header[10]:>8}"
|
||||
)
|
||||
for item in metrics:
|
||||
print(
|
||||
f"{item.stem:42} {item.precision:6.3f} {item.recall:6.3f} {item.f1:6.3f} "
|
||||
f"{item.true_positive:4d} {item.false_positive:4d} {item.false_negative:4d} "
|
||||
f"{item.manual_cells:6d} {item.generated_cells:6d} "
|
||||
f"{item.manual_2x2:11d} {item.generated_2x2:8d}"
|
||||
)
|
||||
|
||||
total_tp = sum(item.true_positive for item in metrics)
|
||||
total_fp = sum(item.false_positive for item in metrics)
|
||||
total_fn = sum(item.false_negative for item in metrics)
|
||||
total_manual = sum(item.manual_cells for item in metrics)
|
||||
total_generated = sum(item.generated_cells for item in metrics)
|
||||
total_manual_2x2 = sum(item.manual_2x2 for item in metrics)
|
||||
total_generated_2x2 = sum(item.generated_2x2 for item in metrics)
|
||||
total = MatrixMetrics(
|
||||
stem="TOTAL",
|
||||
true_positive=total_tp,
|
||||
false_positive=total_fp,
|
||||
false_negative=total_fn,
|
||||
manual_cells=total_manual,
|
||||
generated_cells=total_generated,
|
||||
manual_2x2=total_manual_2x2,
|
||||
generated_2x2=total_generated_2x2,
|
||||
)
|
||||
print("-" * 113)
|
||||
print(
|
||||
f"{total.stem:42} {total.precision:6.3f} {total.recall:6.3f} {total.f1:6.3f} "
|
||||
f"{total.true_positive:4d} {total.false_positive:4d} {total.false_negative:4d} "
|
||||
f"{total.manual_cells:6d} {total.generated_cells:6d} "
|
||||
f"{total.manual_2x2:11d} {total.generated_2x2:8d}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Compare manual 17x17 labels against generated matrix output."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manual-dir",
|
||||
type=Path,
|
||||
default=Path("PreProcessing/manual_matrices"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--generated-dir",
|
||||
type=Path,
|
||||
default=Path("PreProcessing/processed_characters"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--diff-dir",
|
||||
type=Path,
|
||||
default=Path("PreProcessing/manual_diffs"),
|
||||
)
|
||||
parser.add_argument("--scale", type=int, default=24)
|
||||
parser.add_argument(
|
||||
"--include-empty-manual",
|
||||
action="store_true",
|
||||
help="Include manual matrices with no black cells instead of treating them as unlabelled.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
manual_paths = sorted(args.manual_dir.glob("*_matrix17.txt"))
|
||||
if not manual_paths:
|
||||
raise SystemExit(f"No manual matrices found in {args.manual_dir}")
|
||||
|
||||
args.diff_dir.mkdir(parents=True, exist_ok=True)
|
||||
metrics: list[MatrixMetrics] = []
|
||||
missing: list[Path] = []
|
||||
skipped_empty: list[Path] = []
|
||||
|
||||
for manual_path in manual_paths:
|
||||
manual = read_matrix(manual_path)
|
||||
if not args.include_empty_manual and int(manual.sum()) == 0:
|
||||
skipped_empty.append(manual_path)
|
||||
continue
|
||||
|
||||
generated_path = args.generated_dir / manual_path.name
|
||||
if not generated_path.exists():
|
||||
missing.append(generated_path)
|
||||
continue
|
||||
|
||||
generated = read_matrix(generated_path)
|
||||
metrics.append(metrics_for_pair(manual_path, generated_path))
|
||||
stem = manual_path.name.removesuffix("_matrix17.txt")
|
||||
write_image(args.diff_dir / f"{stem}_diff.png", diff_image(manual, generated, args.scale))
|
||||
|
||||
if missing:
|
||||
print("Missing generated matrices:")
|
||||
for path in missing:
|
||||
print(f" {path}")
|
||||
|
||||
if skipped_empty:
|
||||
print("Skipped empty manual matrices:")
|
||||
for path in skipped_empty:
|
||||
print(f" {path}")
|
||||
|
||||
if not metrics:
|
||||
raise SystemExit("No comparable matrix pairs found.")
|
||||
|
||||
print_table(metrics)
|
||||
print(f"\nDiff images written to: {args.diff_dir}")
|
||||
print("Diff colors: black=match, red=generated extra, blue=manual missing.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
364
PreProcessing/scripts/draw_matrix17.py
Normal file
@@ -0,0 +1,364 @@
|
||||
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()
|
||||
143
PreProcessing/scripts/run_preprocessing.py
Normal file
@@ -0,0 +1,143 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from process_characters import (
|
||||
IMAGE_SUFFIXES,
|
||||
binarize_foreground,
|
||||
clean_mask,
|
||||
matrix_map,
|
||||
mask_to_display,
|
||||
read_image,
|
||||
write_image,
|
||||
)
|
||||
|
||||
|
||||
def matrix_to_text(matrix: np.ndarray) -> str:
|
||||
return "\n".join(
|
||||
"".join("1" if value > 0 else "0" for value in row) for row in matrix
|
||||
) + "\n"
|
||||
|
||||
|
||||
def iter_image_paths(input_dir: Path) -> list[Path]:
|
||||
return sorted(
|
||||
path
|
||||
for path in input_dir.iterdir()
|
||||
if path.is_file() and path.suffix.lower() in IMAGE_SUFFIXES
|
||||
)
|
||||
|
||||
|
||||
def process_image(
|
||||
image_path: Path,
|
||||
cleaned_dir: Path,
|
||||
matrix_dir: Path,
|
||||
grid_size: int = 17,
|
||||
cell_samples: int = 32,
|
||||
open_kernel_size: int = 1,
|
||||
close_kernel_size: int = 0,
|
||||
median_size: int = 1,
|
||||
min_component_area: int = 8,
|
||||
triangle_threshold: float = 0.18,
|
||||
) -> None:
|
||||
original = read_image(image_path)
|
||||
gray = cv2.cvtColor(original, cv2.COLOR_BGR2GRAY)
|
||||
binary = binarize_foreground(gray)
|
||||
cleaned = clean_mask(
|
||||
binary,
|
||||
open_kernel_size=open_kernel_size,
|
||||
close_kernel_size=close_kernel_size,
|
||||
median_size=median_size,
|
||||
min_component_area=min_component_area,
|
||||
grid_size=grid_size,
|
||||
)
|
||||
mapping = matrix_map(
|
||||
cleaned,
|
||||
grid_size=grid_size,
|
||||
triangle_threshold=triangle_threshold,
|
||||
cell_samples=cell_samples,
|
||||
)
|
||||
|
||||
stem = image_path.stem
|
||||
write_image(cleaned_dir / f"{stem}_cleaned.png", mask_to_display(cleaned))
|
||||
(matrix_dir / f"{stem}_matrix.txt").write_text(
|
||||
matrix_to_text(mapping.matrix),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def process_directory(
|
||||
input_dir: Path,
|
||||
cleaned_dir: Path,
|
||||
matrix_dir: Path,
|
||||
grid_size: int = 17,
|
||||
cell_samples: int = 32,
|
||||
open_kernel_size: int = 1,
|
||||
close_kernel_size: int = 0,
|
||||
median_size: int = 1,
|
||||
min_component_area: int = 8,
|
||||
triangle_threshold: float = 0.18,
|
||||
) -> int:
|
||||
cleaned_dir.mkdir(parents=True, exist_ok=True)
|
||||
matrix_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
image_paths = iter_image_paths(input_dir)
|
||||
if not image_paths:
|
||||
raise SystemExit(f"No images found in {input_dir}")
|
||||
|
||||
for image_path in image_paths:
|
||||
process_image(
|
||||
image_path=image_path,
|
||||
cleaned_dir=cleaned_dir,
|
||||
matrix_dir=matrix_dir,
|
||||
grid_size=grid_size,
|
||||
cell_samples=cell_samples,
|
||||
open_kernel_size=open_kernel_size,
|
||||
close_kernel_size=close_kernel_size,
|
||||
median_size=median_size,
|
||||
min_component_area=min_component_area,
|
||||
triangle_threshold=triangle_threshold,
|
||||
)
|
||||
print(f"processed: {image_path.name}")
|
||||
|
||||
return len(image_paths)
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Clean seal-script images and export 17x17 matrix text files."
|
||||
)
|
||||
parser.add_argument("--input-dir", type=Path, required=True)
|
||||
parser.add_argument("--cleaned-dir", type=Path, required=True)
|
||||
parser.add_argument("--matrix-dir", type=Path, required=True)
|
||||
parser.add_argument("--grid-size", type=int, default=17)
|
||||
parser.add_argument("--cell-samples", type=int, default=32)
|
||||
parser.add_argument("--open-kernel-size", type=int, default=1)
|
||||
parser.add_argument("--close-kernel-size", type=int, default=0)
|
||||
parser.add_argument("--median-size", type=int, default=1)
|
||||
parser.add_argument("--min-component-area", type=int, default=8)
|
||||
parser.add_argument("--triangle-threshold", type=float, default=0.18)
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = build_parser().parse_args()
|
||||
process_directory(
|
||||
input_dir=args.input_dir,
|
||||
cleaned_dir=args.cleaned_dir,
|
||||
matrix_dir=args.matrix_dir,
|
||||
grid_size=args.grid_size,
|
||||
cell_samples=args.cell_samples,
|
||||
open_kernel_size=args.open_kernel_size,
|
||||
close_kernel_size=args.close_kernel_size,
|
||||
median_size=args.median_size,
|
||||
min_component_area=args.min_component_area,
|
||||
triangle_threshold=args.triangle_threshold,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -3,141 +3,55 @@
|
||||
<title>篆文字_page_017_text_17</title>
|
||||
<rect width="100%" height="100%" fill="white"/>
|
||||
<g fill="black">
|
||||
<rect x="45.0638" y="30.3182" width="15.0213" height="15.1591"/>
|
||||
<rect x="165.2340" y="30.3182" width="15.0213" height="15.1591"/>
|
||||
<rect x="390.5532" y="30.3182" width="285.4043" height="15.1591"/>
|
||||
<rect x="45.0638" y="45.4773" width="300.4255" height="15.1591"/>
|
||||
<rect x="390.5532" y="45.4773" width="285.4043" height="15.1591"/>
|
||||
<rect x="45.0638" y="60.6364" width="285.4043" height="15.1591"/>
|
||||
<rect x="390.5532" y="60.6364" width="15.0213" height="15.1591"/>
|
||||
<rect x="630.8936" y="60.6364" width="45.0638" height="15.1591"/>
|
||||
<rect x="45.0638" y="75.7955" width="30.0426" height="15.1591"/>
|
||||
<rect x="630.8936" y="75.7955" width="45.0638" height="15.1591"/>
|
||||
<rect x="45.0638" y="90.9545" width="30.0426" height="15.1591"/>
|
||||
<rect x="510.7234" y="90.9545" width="165.2340" height="15.1591"/>
|
||||
<rect x="45.0638" y="106.1136" width="300.4255" height="15.1591"/>
|
||||
<rect x="390.5532" y="106.1136" width="285.4043" height="15.1591"/>
|
||||
<rect x="45.0638" y="121.2727" width="300.4255" height="15.1591"/>
|
||||
<rect x="390.5532" y="121.2727" width="240.3404" height="15.1591"/>
|
||||
<rect x="645.9149" y="121.2727" width="30.0426" height="15.1591"/>
|
||||
<rect x="30.0426" y="136.4318" width="45.0638" height="15.1591"/>
|
||||
<rect x="300.4255" y="136.4318" width="45.0638" height="15.1591"/>
|
||||
<rect x="390.5532" y="136.4318" width="30.0426" height="15.1591"/>
|
||||
<rect x="480.6809" y="136.4318" width="15.0213" height="15.1591"/>
|
||||
<rect x="30.0426" y="151.5909" width="45.0638" height="15.1591"/>
|
||||
<rect x="300.4255" y="151.5909" width="30.0426" height="15.1591"/>
|
||||
<rect x="375.5319" y="151.5909" width="45.0638" height="15.1591"/>
|
||||
<rect x="30.0426" y="166.7500" width="315.4468" height="15.1591"/>
|
||||
<rect x="375.5319" y="166.7500" width="45.0638" height="15.1591"/>
|
||||
<rect x="465.6596" y="166.7500" width="210.2979" height="15.1591"/>
|
||||
<rect x="30.0426" y="181.9091" width="300.4255" height="15.1591"/>
|
||||
<rect x="375.5319" y="181.9091" width="45.0638" height="15.1591"/>
|
||||
<rect x="450.6383" y="181.9091" width="225.3191" height="15.1591"/>
|
||||
<rect x="30.0426" y="197.0682" width="45.0638" height="15.1591"/>
|
||||
<rect x="90.1277" y="197.0682" width="30.0426" height="15.1591"/>
|
||||
<rect x="210.2979" y="197.0682" width="60.0851" height="15.1591"/>
|
||||
<rect x="375.5319" y="197.0682" width="45.0638" height="15.1591"/>
|
||||
<rect x="450.6383" y="197.0682" width="45.0638" height="15.1591"/>
|
||||
<rect x="630.8936" y="197.0682" width="45.0638" height="15.1591"/>
|
||||
<rect x="30.0426" y="212.2273" width="45.0638" height="15.1591"/>
|
||||
<rect x="375.5319" y="212.2273" width="45.0638" height="15.1591"/>
|
||||
<rect x="450.6383" y="212.2273" width="45.0638" height="15.1591"/>
|
||||
<rect x="630.8936" y="212.2273" width="45.0638" height="15.1591"/>
|
||||
<rect x="30.0426" y="227.3864" width="75.1064" height="15.1591"/>
|
||||
<rect x="375.5319" y="227.3864" width="45.0638" height="15.1591"/>
|
||||
<rect x="450.6383" y="227.3864" width="45.0638" height="15.1591"/>
|
||||
<rect x="630.8936" y="227.3864" width="45.0638" height="15.1591"/>
|
||||
<rect x="30.0426" y="242.5455" width="300.4255" height="15.1591"/>
|
||||
<rect x="375.5319" y="242.5455" width="45.0638" height="15.1591"/>
|
||||
<rect x="450.6383" y="242.5455" width="45.0638" height="15.1591"/>
|
||||
<rect x="525.7447" y="242.5455" width="150.2128" height="15.1591"/>
|
||||
<rect x="30.0426" y="257.7045" width="300.4255" height="15.1591"/>
|
||||
<rect x="375.5319" y="257.7045" width="45.0638" height="15.1591"/>
|
||||
<rect x="450.6383" y="257.7045" width="45.0638" height="15.1591"/>
|
||||
<rect x="525.7447" y="257.7045" width="150.2128" height="15.1591"/>
|
||||
<rect x="300.4255" y="272.8636" width="30.0426" height="15.1591"/>
|
||||
<rect x="375.5319" y="272.8636" width="45.0638" height="15.1591"/>
|
||||
<rect x="450.6383" y="272.8636" width="45.0638" height="15.1591"/>
|
||||
<rect x="300.4255" y="288.0227" width="30.0426" height="15.1591"/>
|
||||
<rect x="375.5319" y="288.0227" width="45.0638" height="15.1591"/>
|
||||
<rect x="450.6383" y="288.0227" width="45.0638" height="15.1591"/>
|
||||
<rect x="30.0426" y="303.1818" width="30.0426" height="15.1591"/>
|
||||
<rect x="300.4255" y="303.1818" width="30.0426" height="15.1591"/>
|
||||
<rect x="375.5319" y="303.1818" width="45.0638" height="15.1591"/>
|
||||
<rect x="450.6383" y="303.1818" width="45.0638" height="15.1591"/>
|
||||
<rect x="510.7234" y="303.1818" width="165.2340" height="15.1591"/>
|
||||
<rect x="30.0426" y="318.3409" width="300.4255" height="15.1591"/>
|
||||
<rect x="375.5319" y="318.3409" width="45.0638" height="15.1591"/>
|
||||
<rect x="450.6383" y="318.3409" width="225.3191" height="15.1591"/>
|
||||
<rect x="30.0426" y="333.5000" width="255.3617" height="15.1591"/>
|
||||
<rect x="315.4468" y="333.5000" width="15.0213" height="15.1591"/>
|
||||
<rect x="375.5319" y="333.5000" width="45.0638" height="15.1591"/>
|
||||
<rect x="450.6383" y="333.5000" width="165.2340" height="15.1591"/>
|
||||
<rect x="630.8936" y="333.5000" width="45.0638" height="15.1591"/>
|
||||
<rect x="30.0426" y="348.6591" width="45.0638" height="15.1591"/>
|
||||
<rect x="375.5319" y="348.6591" width="45.0638" height="15.1591"/>
|
||||
<rect x="630.8936" y="348.6591" width="45.0638" height="15.1591"/>
|
||||
<rect x="30.0426" y="363.8182" width="45.0638" height="15.1591"/>
|
||||
<rect x="375.5319" y="363.8182" width="45.0638" height="15.1591"/>
|
||||
<rect x="630.8936" y="363.8182" width="45.0638" height="15.1591"/>
|
||||
<rect x="30.0426" y="378.9773" width="45.0638" height="15.1591"/>
|
||||
<rect x="90.1277" y="378.9773" width="15.0213" height="15.1591"/>
|
||||
<rect x="300.4255" y="378.9773" width="30.0426" height="15.1591"/>
|
||||
<rect x="375.5319" y="378.9773" width="30.0426" height="15.1591"/>
|
||||
<rect x="540.7660" y="378.9773" width="135.1915" height="15.1591"/>
|
||||
<rect x="30.0426" y="394.1364" width="300.4255" height="15.1591"/>
|
||||
<rect x="375.5319" y="394.1364" width="45.0638" height="15.1591"/>
|
||||
<rect x="450.6383" y="394.1364" width="225.3191" height="15.1591"/>
|
||||
<rect x="30.0426" y="409.2955" width="300.4255" height="15.1591"/>
|
||||
<rect x="375.5319" y="409.2955" width="30.0426" height="15.1591"/>
|
||||
<rect x="450.6383" y="409.2955" width="120.1702" height="15.1591"/>
|
||||
<rect x="285.4043" y="424.4545" width="45.0638" height="15.1591"/>
|
||||
<rect x="375.5319" y="424.4545" width="45.0638" height="15.1591"/>
|
||||
<rect x="465.6596" y="424.4545" width="30.0426" height="15.1591"/>
|
||||
<rect x="285.4043" y="439.6136" width="45.0638" height="15.1591"/>
|
||||
<rect x="375.5319" y="439.6136" width="45.0638" height="15.1591"/>
|
||||
<rect x="465.6596" y="439.6136" width="30.0426" height="15.1591"/>
|
||||
<rect x="30.0426" y="454.7727" width="300.4255" height="15.1591"/>
|
||||
<rect x="375.5319" y="454.7727" width="30.0426" height="15.1591"/>
|
||||
<rect x="465.6596" y="454.7727" width="210.2979" height="15.1591"/>
|
||||
<rect x="30.0426" y="469.9318" width="300.4255" height="15.1591"/>
|
||||
<rect x="375.5319" y="469.9318" width="30.0426" height="15.1591"/>
|
||||
<rect x="465.6596" y="469.9318" width="210.2979" height="15.1591"/>
|
||||
<rect x="30.0426" y="485.0909" width="45.0638" height="15.1591"/>
|
||||
<rect x="150.2128" y="485.0909" width="120.1702" height="15.1591"/>
|
||||
<rect x="285.4043" y="485.0909" width="30.0426" height="15.1591"/>
|
||||
<rect x="360.5106" y="485.0909" width="45.0638" height="15.1591"/>
|
||||
<rect x="465.6596" y="485.0909" width="210.2979" height="15.1591"/>
|
||||
<rect x="30.0426" y="500.2500" width="45.0638" height="15.1591"/>
|
||||
<rect x="375.5319" y="500.2500" width="30.0426" height="15.1591"/>
|
||||
<rect x="630.8936" y="500.2500" width="45.0638" height="15.1591"/>
|
||||
<rect x="30.0426" y="515.4091" width="45.0638" height="15.1591"/>
|
||||
<rect x="375.5319" y="515.4091" width="30.0426" height="15.1591"/>
|
||||
<rect x="645.9149" y="515.4091" width="30.0426" height="15.1591"/>
|
||||
<rect x="30.0426" y="530.5682" width="285.4043" height="15.1591"/>
|
||||
<rect x="360.5106" y="530.5682" width="45.0638" height="15.1591"/>
|
||||
<rect x="465.6596" y="530.5682" width="210.2979" height="15.1591"/>
|
||||
<rect x="30.0426" y="545.7273" width="300.4255" height="15.1591"/>
|
||||
<rect x="360.5106" y="545.7273" width="45.0638" height="15.1591"/>
|
||||
<rect x="465.6596" y="545.7273" width="210.2979" height="15.1591"/>
|
||||
<rect x="150.2128" y="560.8864" width="15.0213" height="15.1591"/>
|
||||
<rect x="225.3191" y="560.8864" width="30.0426" height="15.1591"/>
|
||||
<rect x="270.3830" y="560.8864" width="60.0851" height="15.1591"/>
|
||||
<rect x="360.5106" y="560.8864" width="45.0638" height="15.1591"/>
|
||||
<rect x="465.6596" y="560.8864" width="30.0426" height="15.1591"/>
|
||||
<rect x="285.4043" y="576.0455" width="45.0638" height="15.1591"/>
|
||||
<rect x="360.5106" y="576.0455" width="45.0638" height="15.1591"/>
|
||||
<rect x="465.6596" y="576.0455" width="30.0426" height="15.1591"/>
|
||||
<rect x="30.0426" y="591.2045" width="15.0213" height="15.1591"/>
|
||||
<rect x="90.1277" y="591.2045" width="60.0851" height="15.1591"/>
|
||||
<rect x="165.2340" y="591.2045" width="165.2340" height="15.1591"/>
|
||||
<rect x="360.5106" y="591.2045" width="45.0638" height="15.1591"/>
|
||||
<rect x="450.6383" y="591.2045" width="225.3191" height="15.1591"/>
|
||||
<rect x="30.0426" y="606.3636" width="300.4255" height="15.1591"/>
|
||||
<rect x="360.5106" y="606.3636" width="45.0638" height="15.1591"/>
|
||||
<rect x="450.6383" y="606.3636" width="225.3191" height="15.1591"/>
|
||||
<rect x="30.0426" y="621.5227" width="285.4043" height="15.1591"/>
|
||||
<rect x="360.5106" y="621.5227" width="45.0638" height="15.1591"/>
|
||||
<rect x="450.6383" y="621.5227" width="225.3191" height="15.1591"/>
|
||||
<rect x="29.0000" y="25.0000" width="304.0000" height="36.0000"/>
|
||||
<rect x="371.0000" y="25.0000" width="304.0000" height="36.0000"/>
|
||||
<rect x="29.0000" y="61.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="637.0000" y="61.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="29.0000" y="97.0000" width="304.0000" height="36.0000"/>
|
||||
<rect x="371.0000" y="97.0000" width="304.0000" height="36.0000"/>
|
||||
<rect x="29.0000" y="133.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="295.0000" y="133.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="371.0000" y="133.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="29.0000" y="169.0000" width="304.0000" height="37.0000"/>
|
||||
<rect x="371.0000" y="169.0000" width="38.0000" height="37.0000"/>
|
||||
<rect x="447.0000" y="169.0000" width="228.0000" height="37.0000"/>
|
||||
<rect x="29.0000" y="206.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="371.0000" y="206.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="447.0000" y="206.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="637.0000" y="206.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="29.0000" y="242.0000" width="304.0000" height="36.0000"/>
|
||||
<rect x="371.0000" y="242.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="447.0000" y="242.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="523.0000" y="242.0000" width="152.0000" height="36.0000"/>
|
||||
<rect x="295.0000" y="278.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="371.0000" y="278.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="447.0000" y="278.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="29.0000" y="314.0000" width="304.0000" height="36.0000"/>
|
||||
<rect x="371.0000" y="314.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="447.0000" y="314.0000" width="228.0000" height="36.0000"/>
|
||||
<rect x="29.0000" y="350.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="371.0000" y="350.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="637.0000" y="350.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="29.0000" y="386.0000" width="304.0000" height="36.0000"/>
|
||||
<rect x="371.0000" y="386.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="447.0000" y="386.0000" width="228.0000" height="36.0000"/>
|
||||
<rect x="295.0000" y="422.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="371.0000" y="422.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="447.0000" y="422.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="29.0000" y="458.0000" width="304.0000" height="37.0000"/>
|
||||
<rect x="371.0000" y="458.0000" width="38.0000" height="37.0000"/>
|
||||
<rect x="447.0000" y="458.0000" width="228.0000" height="37.0000"/>
|
||||
<rect x="29.0000" y="495.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="371.0000" y="495.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="637.0000" y="495.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="29.0000" y="531.0000" width="304.0000" height="36.0000"/>
|
||||
<rect x="371.0000" y="531.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="447.0000" y="531.0000" width="228.0000" height="36.0000"/>
|
||||
<rect x="295.0000" y="567.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="371.0000" y="567.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="447.0000" y="567.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="29.0000" y="603.0000" width="304.0000" height="36.0000"/>
|
||||
<rect x="371.0000" y="603.0000" width="38.0000" height="36.0000"/>
|
||||
<rect x="447.0000" y="603.0000" width="228.0000" height="36.0000"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 3.7 KiB |