Files
Project_ZHUAN/PreProcessing/scripts/process_characters.py
CrbnsCat10n 60dbc51403 Baseline
2026-05-11 13:42:51 +08:00

265 lines
7.5 KiB
Python

from __future__ import annotations
import argparse
import html
import os
from pathlib import Path
import cv2
os.environ.setdefault(
"MPLCONFIGDIR",
str(Path(__file__).resolve().parents[1] / ".matplotlib-cache"),
)
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".tif", ".tiff", ".bmp", ".webp"}
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 binarize_foreground(gray: np.ndarray) -> np.ndarray:
blurred = cv2.GaussianBlur(gray, (3, 3), 0)
threshold, _ = cv2.threshold(
blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU
)
border = np.concatenate(
[
gray[0, :],
gray[-1, :],
gray[:, 0],
gray[:, -1],
]
)
background_is_light = np.median(border) >= 128
if background_is_light:
foreground = (gray < threshold).astype(np.uint8) * 255
else:
foreground = (gray > threshold).astype(np.uint8) * 255
return foreground
def clean_mask(
mask: np.ndarray,
open_kernel_size: int,
close_kernel_size: int,
median_size: int,
) -> np.ndarray:
if median_size > 1:
if median_size % 2 == 0:
median_size += 1
mask = cv2.medianBlur(mask, median_size)
open_kernel = cv2.getStructuringElement(
cv2.MORPH_RECT, (open_kernel_size, open_kernel_size)
)
opened = cv2.morphologyEx(mask, cv2.MORPH_OPEN, open_kernel)
close_kernel = cv2.getStructuringElement(
cv2.MORPH_RECT, (close_kernel_size, close_kernel_size)
)
return cv2.morphologyEx(opened, cv2.MORPH_CLOSE, close_kernel)
def pixelate(mask: np.ndarray, factor: int) -> tuple[np.ndarray, np.ndarray]:
height, width = mask.shape
small_width = max(1, round(width / factor))
small_height = max(1, round(height / factor))
sampled = cv2.resize(
mask, (small_width, small_height), interpolation=cv2.INTER_AREA
)
small = np.where(sampled >= 96, 255, 0).astype(np.uint8)
large = cv2.resize(small, (width, height), interpolation=cv2.INTER_NEAREST)
return small, large
def mask_to_display(mask: np.ndarray) -> np.ndarray:
return np.where(mask > 0, 0, 255).astype(np.uint8)
def save_comparison(
path: Path,
original_bgr: np.ndarray,
cleaned_mask: np.ndarray,
pixelated_mask: np.ndarray,
) -> None:
original_rgb = cv2.cvtColor(original_bgr, cv2.COLOR_BGR2RGB)
cleaned = mask_to_display(cleaned_mask)
pixelated = mask_to_display(pixelated_mask)
fig, axes = plt.subplots(1, 3, figsize=(12, 4), constrained_layout=True)
panels = [
("Original", original_rgb, None),
("Cleaned binary", cleaned, "gray"),
("Pixelated", pixelated, "gray"),
]
for axis, (title, image, cmap) in zip(axes, panels):
axis.imshow(image, cmap=cmap)
axis.set_title(title)
axis.axis("off")
fig.savefig(path, dpi=180)
plt.close(fig)
def small_matrix_to_svg(
small_mask: np.ndarray,
original_width: int,
original_height: int,
title: str,
) -> str:
rows, cols = small_mask.shape
cell_width = original_width / cols
cell_height = original_height / rows
escaped_title = html.escape(title)
parts = [
'<?xml version="1.0" encoding="UTF-8"?>',
(
f'<svg xmlns="http://www.w3.org/2000/svg" '
f'width="{original_width}" height="{original_height}" '
f'viewBox="0 0 {original_width} {original_height}" '
f'shape-rendering="crispEdges">'
),
f" <title>{escaped_title}</title>",
' <rect width="100%" height="100%" fill="white"/>',
' <g fill="black">',
]
for row_index in range(rows):
col_index = 0
while col_index < cols:
if small_mask[row_index, col_index] == 0:
col_index += 1
continue
run_start = col_index
while col_index < cols and small_mask[row_index, col_index] > 0:
col_index += 1
x = run_start * cell_width
y = row_index * cell_height
width = (col_index - run_start) * cell_width
parts.append(
f' <rect x="{x:.4f}" y="{y:.4f}" '
f'width="{width:.4f}" height="{cell_height:.4f}"/>'
)
parts.extend([" </g>", "</svg>", ""])
return "\n".join(parts)
def process_image(
image_path: Path,
processed_dir: Path,
comparison_dir: Path,
svg_dir: Path,
factor: int,
open_kernel_size: int,
close_kernel_size: int,
median_size: int,
) -> 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,
)
small, pixelated = pixelate(cleaned, factor=factor)
stem = image_path.stem
write_image(processed_dir / f"{stem}_cleaned.png", mask_to_display(cleaned))
write_image(processed_dir / f"{stem}_pixelated.png", mask_to_display(pixelated))
save_comparison(
comparison_dir / f"{stem}_comparison.png", original, cleaned, pixelated
)
height, width = gray.shape
svg = small_matrix_to_svg(small, width, height, stem)
(svg_dir / f"{stem}_pixelated.svg").write_text(svg, encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser(
description="Clean, pixelate, and vectorize scanned seal-script characters."
)
parser.add_argument(
"--input-dir",
type=Path,
default=Path("PreProcessing/original_characters"),
)
parser.add_argument(
"--processed-dir",
type=Path,
default=Path("PreProcessing/processed_characters"),
)
parser.add_argument(
"--comparison-dir",
type=Path,
default=Path("PreProcessing/comparisons"),
)
parser.add_argument(
"--svg-dir",
type=Path,
default=Path("PreProcessing/svg_characters"),
)
parser.add_argument("--factor", type=int, default=15)
parser.add_argument("--open-kernel-size", type=int, default=2)
parser.add_argument("--close-kernel-size", type=int, default=3)
parser.add_argument("--median-size", type=int, default=5)
args = parser.parse_args()
for directory in (args.processed_dir, args.comparison_dir, args.svg_dir):
directory.mkdir(parents=True, exist_ok=True)
images = sorted(
path
for path in args.input_dir.iterdir()
if path.is_file() and path.suffix.lower() in IMAGE_SUFFIXES
)
if not images:
raise SystemExit(f"No images found in {args.input_dir}")
for image_path in images:
process_image(
image_path=image_path,
processed_dir=args.processed_dir,
comparison_dir=args.comparison_dir,
svg_dir=args.svg_dir,
factor=args.factor,
open_kernel_size=args.open_kernel_size,
close_kernel_size=args.close_kernel_size,
median_size=args.median_size,
)
print(f"processed: {image_path.name}")
if __name__ == "__main__":
main()