From 46c2ad1f3620a6ba1c415dce866f9f91d95c81dc Mon Sep 17 00:00:00 2001 From: DC_Provide Date: Sat, 9 May 2026 20:45:14 +0800 Subject: [PATCH] 111 --- autoCut.py | 209 +++++++++++++++++++ guiCut.py | 551 +++++++++++++++++++++++++++++++++++++++++++++++++ labelme_cut.py | 63 ++++++ list_dir.py | 10 + main.py | 16 ++ 5 files changed, 849 insertions(+) create mode 100644 autoCut.py create mode 100644 guiCut.py create mode 100644 labelme_cut.py create mode 100644 list_dir.py create mode 100644 main.py diff --git a/autoCut.py b/autoCut.py new file mode 100644 index 0000000..6563527 --- /dev/null +++ b/autoCut.py @@ -0,0 +1,209 @@ +import cv2 +import numpy as np +import os +from pathlib import Path + +# ========== 基础配置(只用填这几个,不用改其他) ========== +INPUT_FOLDER = "pages" +OUTPUT_FOLDER = "chars_auto" + +# 第一个字中心点、最后一个字中心点 +CX0, CY0 = 1027, 1257 +CX1, CY1 = 4029, 5673 + +CHAR_SIZE = 715 +ROWS = 5 +COLS = 5 + +# 搜索范围扩展(像素) +SEARCH_PAD = 150 + +# 偏移修正最大幅度(像素) +MAX_SHIFT = 100 +# ========================================================== + +Path(OUTPUT_FOLDER).mkdir(exist_ok=True) + +# 兼容中文路径读图 +def imread_unicode(path): + return cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR) + +# 兼容中文路径存图 +def imwrite_unicode(path, img): + cv2.imencode('.png', img)[1].tofile(path) + +def get_char_center_from_bbox( + img_gray, + roi_x1, + roi_y1, + roi_x2, + roi_y2, + target_cx=None, + target_cy=None, + max_shift=MAX_SHIFT, +): + """ + 在理论区域内,自动计算文字的真实边界框中心 + 返回:真实中心点 (cx, cy) + """ + # 截取局部ROI + roi = img_gray[roi_y1:roi_y2, roi_x1:roi_x2] + roi_h, roi_w = roi.shape[:2] + + blur = cv2.GaussianBlur(roi, (5, 5), 0) + _, binary = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) + + kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) + binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations=1) + + num_labels, _, stats, centroids = cv2.connectedComponentsWithStats(binary, connectivity=8) + if num_labels <= 1: + if target_cx is not None and target_cy is not None: + return float(target_cx), float(target_cy) + cx = (roi_x1 + roi_x2) / 2.0 + cy = (roi_y1 + roi_y2) / 2.0 + return cx, cy + + roi_area = float(roi_h * roi_w) + min_area = max(300.0, roi_area * 0.002) + + if target_cx is None: + tx = roi_w / 2.0 + else: + tx = float(target_cx - roi_x1) + if target_cy is None: + ty = roi_h / 2.0 + else: + ty = float(target_cy - roi_y1) + + gate = float(max(40.0, float(max_shift) + 20.0)) + + best_score = None + best_center = None + for i in range(1, num_labels): + x, y, w, h, area = stats[i] + area = float(area) + if area < min_area: + continue + + box_area = float(w * h) if w > 0 and h > 0 else 1.0 + fill = area / box_area + + if (w > 0.85 * roi_w or h > 0.85 * roi_h) and fill < 0.08: + continue + + aspect = float(w) / (float(h) + 1e-6) + if (aspect > 12.0 or aspect < (1.0 / 12.0)) and fill < 0.15: + continue + + cx, cy = centroids[i] + if abs(float(cx) - tx) > gate or abs(float(cy) - ty) > gate: + continue + dx = (float(cx) - tx) / (roi_w + 1e-6) + dy = (float(cy) - ty) / (roi_h + 1e-6) + dist2 = dx * dx + dy * dy + score = area * (fill + 0.2) * np.exp(-16.0 * dist2) + + if best_score is None or score > best_score: + best_score = score + best_center = (float(cx), float(cy)) + + if best_center is None: + min_area_relaxed = max(100.0, roi_area * 0.0005) + for i in range(1, num_labels): + x, y, w, h, area = stats[i] + area = float(area) + if area < min_area_relaxed: + continue + + box_area = float(w * h) if w > 0 and h > 0 else 1.0 + fill = area / box_area + if (w > 0.90 * roi_w or h > 0.90 * roi_h) and fill < 0.06: + continue + + cx, cy = centroids[i] + if abs(float(cx) - tx) > gate or abs(float(cy) - ty) > gate: + continue + score = area * (fill + 0.2) + if best_score is None or score > best_score: + best_score = score + best_center = (float(cx), float(cy)) + + if best_center is None: + if target_cx is not None and target_cy is not None: + return float(target_cx), float(target_cy) + cx = (roi_x1 + roi_x2) / 2.0 + cy = (roi_y1 + roi_y2) / 2.0 + return cx, cy + + return roi_x1 + best_center[0], roi_y1 + best_center[1] + +def process_page(img_path, page_idx): + img = imread_unicode(str(img_path)) + if img is None: + print(f"跳过无法读取:{img_path.name}") + return + h, w = img.shape[:2] + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + + # 生成理论网格行列间隔 + step_x = (CX1 - CX0) / (COLS - 1) + step_y = (CY1 - CY0) / (ROWS - 1) + + for row in range(ROWS): + for col in range(COLS): + # 理论中心点 + theo_cx = CX0 + col * step_x + theo_cy = CY0 + row * step_y + + # 理论区域ROI范围 + r_x1 = int(theo_cx - CHAR_SIZE // 2 - SEARCH_PAD) + r_y1 = int(theo_cy - CHAR_SIZE // 2 - SEARCH_PAD) + r_x2 = int(theo_cx + CHAR_SIZE // 2 + SEARCH_PAD) + r_y2 = int(theo_cy + CHAR_SIZE // 2 + SEARCH_PAD) + + # 边界保护 + r_x1 = max(0, r_x1) + r_y1 = max(0, r_y1) + r_x2 = min(w, r_x2) + r_y2 = min(h, r_y2) + + # 自动获取真实文字边界框中心(只允许小幅修正) + detect_cx, detect_cy = get_char_center_from_bbox( + gray, r_x1, r_y1, r_x2, r_y2, theo_cx, theo_cy, max_shift=MAX_SHIFT + ) + dx = float(detect_cx - theo_cx) + dy = float(detect_cy - theo_cy) + dx = float(np.clip(dx, -MAX_SHIFT, MAX_SHIFT)) + dy = float(np.clip(dy, -MAX_SHIFT, MAX_SHIFT)) + real_cx = float(theo_cx + dx) + real_cy = float(theo_cy + dy) + + # 用真实中心裁切 715x715 + x1 = int(round(real_cx - CHAR_SIZE / 2.0)) + y1 = int(round(real_cy - CHAR_SIZE / 2.0)) + x2 = x1 + CHAR_SIZE + y2 = y1 + CHAR_SIZE + + canvas = np.full((CHAR_SIZE, CHAR_SIZE, 3), 255, dtype=np.uint8) + sx1 = max(0, x1) + sy1 = max(0, y1) + sx2 = min(w, x2) + sy2 = min(h, y2) + src = img[sy1:sy2, sx1:sx2] + + dx = max(0, -x1) + dy = max(0, -y1) + canvas[dy:dy + src.shape[0], dx:dx + src.shape[1]] = src + crop = canvas + fname = f"page{page_idx}_r{row}_c{col}.png" + save_path = os.path.join(OUTPUT_FOLDER, fname) + imwrite_unicode(save_path, crop) + +if __name__ == "__main__": + print("开始 5×5 逐字自动边界检测中心定位切割...") + img_list = sorted(Path(INPUT_FOLDER).glob("*.png")) + for idx, f in enumerate(img_list): + process_page(f, idx + 1) + print(f"已完成:{f.name}") + print("✅ 全部自动切割+自动边界检测定位完成!") diff --git a/guiCut.py b/guiCut.py new file mode 100644 index 0000000..d60bbde --- /dev/null +++ b/guiCut.py @@ -0,0 +1,551 @@ +import base64 +import math +import os +from pathlib import Path +import tkinter as tk +from tkinter import messagebox + +import cv2 +import numpy as np + + +INPUT_FOLDER = "pages" +OUTPUT_FOLDER = "chars_gui" + +CX0, CY0 = 1027, 1257 +CX1, CY1 = 4029, 5673 + +CHAR_W = 700 +CHAR_H = 819 +ROWS = 5 +COLS = 5 + +OFFSET_MIN = -300 +OFFSET_MAX = 300 + +SCALE_MIN = 0.70 +SCALE_MAX = 1.30 +SCALE_RES = 0.001 + +ROT_MIN = -30 +ROT_MAX = 30 +ROT_RES = 0.1 + +DISPLAY_MAX_W = 1100 +DISPLAY_MAX_H = 900 + + +def imread_unicode(path: str): + return cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR) + + +def imwrite_unicode(path: str, img: np.ndarray): + cv2.imencode(".png", img)[1].tofile(path) + + +def clamp_int(v: int, lo: int, hi: int) -> int: + return int(max(lo, min(hi, int(v)))) + +def clamp_float(v: float, lo: float, hi: float) -> float: + return float(max(lo, min(hi, float(v)))) + + +def crop_fixed(img: np.ndarray, x1: int, y1: int, size: int) -> np.ndarray: + h, w = img.shape[:2] + x2 = x1 + size + y2 = y1 + size + + out = np.full((size, size, 3), 255, dtype=np.uint8) + + sx1 = max(0, x1) + sy1 = max(0, y1) + sx2 = min(w, x2) + sy2 = min(h, y2) + + src = img[sy1:sy2, sx1:sx2] + if src.size == 0: + return out + + dx = max(0, -x1) + dy = max(0, -y1) + out[dy : dy + src.shape[0], dx : dx + src.shape[1]] = src + return out + + +class PageCutterApp: + def __init__(self, root: tk.Tk): + self.root = root + self.root.title("篆文 5×5 分块裁切(整体偏移 / 缩放 / 旋转)") + + self.pages = sorted(Path(INPUT_FOLDER).glob("*.png")) + if not self.pages: + messagebox.showerror("错误", f"未找到图片:{Path(INPUT_FOLDER).resolve()}") + raise SystemExit(1) + + self.page_index = 0 + self.src_bgr = None + self.src_h = 0 + self.src_w = 0 + self.scale = 1.0 + + self.offset_x = tk.IntVar(value=0) + self.offset_y = tk.IntVar(value=0) + self.factor = tk.DoubleVar(value=1.0) + self.angle = tk.DoubleVar(value=0.0) + + self.mode = tk.StringVar(value="all") + self.selected_row = tk.IntVar(value=1) + self.params_all = {"dx": 0, "dy": 0, "factor": 1.0, "angle": 0.0} + self.params_row = {r: {"dx": 0, "dy": 0, "factor": 1.0, "angle": 0.0} for r in range(1, ROWS + 1)} + self._suspend = False + self._notice_after = None + + self._dragging = False + self._drag_start = None + + self._build_ui() + self._bind_keys() + self.mode.trace_add("write", lambda *_: self._on_mode_or_row_change()) + self.selected_row.trace_add("write", lambda *_: self._on_mode_or_row_change()) + + self._load_page(0) + + def _save_current_params(self): + mode = str(self.mode.get()) + if mode == "all": + store = self.params_all + else: + r = clamp_int(int(self.selected_row.get()), 1, ROWS) + store = self.params_row[r] + store["dx"] = int(self.offset_x.get()) + store["dy"] = int(self.offset_y.get()) + store["factor"] = float(self.factor.get()) + store["angle"] = float(self.angle.get()) + + def _load_params(self): + mode = str(self.mode.get()) + if mode == "all": + store = self.params_all + else: + r = clamp_int(int(self.selected_row.get()), 1, ROWS) + store = self.params_row[r] + self.offset_x.set(int(store["dx"])) + self.offset_y.set(int(store["dy"])) + self.factor.set(float(store["factor"])) + self.angle.set(float(store["angle"])) + + def _on_mode_or_row_change(self): + if self._suspend: + return + self._save_current_params() + self._suspend = True + self._load_params() + self._suspend = False + self._update_boxes() + + def _build_ui(self): + self.root.geometry("1400x980") + + main = tk.Frame(self.root) + main.pack(fill=tk.BOTH, expand=True) + + left = tk.Frame(main) + left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + right = tk.Frame(main, width=260) + right.pack(side=tk.RIGHT, fill=tk.Y) + right.pack_propagate(False) + + self.canvas = tk.Canvas(left, bg="#202020", highlightthickness=0) + self.canvas.pack(fill=tk.BOTH, expand=True) + + self.status = tk.Label(right, text="", anchor="w", justify="left") + self.status.pack(fill=tk.X, padx=10, pady=(10, 6)) + + mode_frame = tk.Frame(right) + mode_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) + tk.Label(mode_frame, text="模式").pack(anchor="w") + tk.Radiobutton(mode_frame, text="25 框", variable=self.mode, value="all").pack(anchor="w") + tk.Radiobutton(mode_frame, text="单行 5 框", variable=self.mode, value="row").pack(anchor="w") + + row_frame = tk.Frame(right) + row_frame.pack(fill=tk.X, padx=10, pady=(0, 12)) + tk.Label(row_frame, text="选择行").pack(anchor="w") + self.row_menu = tk.OptionMenu(row_frame, self.selected_row, *list(range(1, ROWS + 1))) + self.row_menu.pack(fill=tk.X) + + tk.Label(right, text="整体偏移 X").pack(anchor="w", padx=10) + self.scale_x = tk.Scale( + right, + from_=OFFSET_MIN, + to=OFFSET_MAX, + orient=tk.HORIZONTAL, + variable=self.offset_x, + command=lambda _=None: self._update_boxes(), + ) + self.scale_x.pack(fill=tk.X, padx=10, pady=(0, 10)) + + tk.Label(right, text="整体偏移 Y").pack(anchor="w", padx=10) + self.scale_y = tk.Scale( + right, + from_=OFFSET_MIN, + to=OFFSET_MAX, + orient=tk.HORIZONTAL, + variable=self.offset_y, + command=lambda _=None: self._update_boxes(), + ) + self.scale_y.pack(fill=tk.X, padx=10, pady=(0, 12)) + + tk.Label(right, text="整体缩放").pack(anchor="w", padx=10) + self.scale_factor = tk.Scale( + right, + from_=SCALE_MIN, + to=SCALE_MAX, + resolution=SCALE_RES, + orient=tk.HORIZONTAL, + variable=self.factor, + command=lambda _=None: self._update_boxes(), + ) + self.scale_factor.pack(fill=tk.X, padx=10, pady=(0, 12)) + + tk.Label(right, text="整体旋转(°)").pack(anchor="w", padx=10) + self.scale_angle = tk.Scale( + right, + from_=ROT_MIN, + to=ROT_MAX, + resolution=ROT_RES, + orient=tk.HORIZONTAL, + variable=self.angle, + command=lambda _=None: self._update_boxes(), + ) + self.scale_angle.pack(fill=tk.X, padx=10, pady=(0, 12)) + + btn_row = tk.Frame(right) + btn_row.pack(fill=tk.X, padx=10, pady=(0, 10)) + + self.btn_prev = tk.Button(btn_row, text="上一页", command=self.prev_page) + self.btn_prev.pack(side=tk.LEFT, fill=tk.X, expand=True) + + self.btn_next = tk.Button(btn_row, text="下一页", command=self.next_page) + self.btn_next.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(8, 0)) + + self.btn_reset = tk.Button(right, text="重置参数", command=self.reset_offset) + self.btn_reset.pack(fill=tk.X, padx=10, pady=(0, 10)) + + self.btn_save = tk.Button(right, text="保存本页 25 个字", command=self.save_page) + self.btn_save.pack(fill=tk.X, padx=10, pady=(0, 10)) + + self.help = tk.Label( + right, + text="拖拽:整体移动\n滚轮:缩放(Shift 精细)\n方向键:微调(Shift 加速)\nW/S:选上一/下一行(自动切到单行)\nA/D:旋转 -/+0.1°\nQ:保存 R:重置 Z/C:上一/下一页", + anchor="w", + justify="left", + ) + self.help.pack(fill=tk.X, padx=10, pady=(0, 10)) + + self.notice = tk.Label(right, text="", anchor="e", justify="right", fg="#00ff7f") + self.notice.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=10) + + self.canvas.bind("", self._on_mouse_down) + self.canvas.bind("", self._on_mouse_drag) + self.canvas.bind("", self._on_mouse_up) + self.canvas.bind("", self._on_mouse_wheel) + + self._photo = None + self._img_item = None + self._boxes = [] + + def _bind_keys(self): + self.root.bind("", self._on_arrow) + self.root.bind("", self._on_arrow) + self.root.bind("", self._on_arrow) + self.root.bind("", self._on_arrow) + self.root.bind("", lambda _=None: self.save_page()) + self.root.bind("q", lambda _=None: self.save_page()) + self.root.bind("Q", lambda _=None: self.save_page()) + self.root.bind("r", lambda _=None: self.reset_offset()) + self.root.bind("R", lambda _=None: self.reset_offset()) + self.root.bind("w", lambda _=None: self._select_row(-1)) + self.root.bind("W", lambda _=None: self._select_row(-1)) + self.root.bind("s", lambda _=None: self._select_row(+1)) + self.root.bind("S", lambda _=None: self._select_row(+1)) + self.root.bind("a", lambda _=None: self._rotate(-ROT_RES)) + self.root.bind("A", lambda _=None: self._rotate(-ROT_RES)) + self.root.bind("d", lambda _=None: self._rotate(+ROT_RES)) + self.root.bind("D", lambda _=None: self._rotate(+ROT_RES)) + self.root.bind("z", lambda _=None: self.prev_page()) + self.root.bind("Z", lambda _=None: self.next_page()) + self.root.bind("C", lambda _=None: self.prev_page()) + self.root.bind("c", lambda _=None: self.next_page()) + + def _set_notice(self, text: str, ok: bool = True): + if self._notice_after is not None: + try: + self.root.after_cancel(self._notice_after) + except Exception: + pass + self._notice_after = None + self.notice.config(text=str(text), fg=("#00ff7f" if ok else "#ff6b6b")) + self._notice_after = self.root.after(2500, lambda: self.notice.config(text="")) + + def _select_row(self, delta: int): + if str(self.mode.get()) != "row": + self.mode.set("row") + r = clamp_int(int(self.selected_row.get()) + int(delta), 1, ROWS) + self.selected_row.set(r) + + def _rotate(self, delta_deg: float): + v = float(self.angle.get()) + float(delta_deg) + self.angle.set(clamp_float(v, float(ROT_MIN), float(ROT_MAX))) + self._update_boxes() + + def _on_mouse_wheel(self, e): + if self._suspend: + return + direction = 1.0 if e.delta > 0 else -1.0 + step = 0.001 if (e.state & 0x0001) else 0.005 + v = float(self.factor.get()) + direction * step + self.factor.set(clamp_float(v, float(SCALE_MIN), float(SCALE_MAX))) + self._update_boxes() + + def _grid_centers(self): + step_x = (CX1 - CX0) / (COLS - 1) + step_y = (CY1 - CY0) / (ROWS - 1) + centers = [] + for r in range(ROWS): + for c in range(COLS): + centers.append((CX0 + c * step_x, CY0 + r * step_y)) + return centers + + def _load_page(self, index: int): + index = clamp_int(index, 0, len(self.pages) - 1) + self.page_index = index + + img_path = self.pages[self.page_index] + bgr = imread_unicode(str(img_path)) + if bgr is None: + messagebox.showerror("错误", f"无法读取:{img_path}") + return + + self.src_bgr = bgr + self.src_h, self.src_w = bgr.shape[:2] + + scale = min(DISPLAY_MAX_W / self.src_w, DISPLAY_MAX_H / self.src_h, 1.0) + self.scale = float(scale) + + rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) + if self.scale != 1.0: + rgb = cv2.resize(rgb, (int(self.src_w * self.scale), int(self.src_h * self.scale)), interpolation=cv2.INTER_AREA) + + ok, buf = cv2.imencode(".png", rgb) + if not ok: + messagebox.showerror("错误", "图片编码失败") + return + + b64 = base64.b64encode(buf).decode("ascii") + self._photo = tk.PhotoImage(data=b64) + + self.canvas.delete("all") + self._img_item = self.canvas.create_image(0, 0, anchor="nw", image=self._photo) + self.canvas.config(width=self._photo.width(), height=self._photo.height(), scrollregion=(0, 0, self._photo.width(), self._photo.height())) + + self._boxes = [] + for _ in range(ROWS * COLS): + rid = self.canvas.create_polygon(0, 0, 0, 0, 0, 0, 0, 0, outline="#00ff7f", fill="", width=2) + self._boxes.append(rid) + + self._update_boxes() + + def _selected_indices(self): + mode = str(self.mode.get()) + if mode == "all": + return list(range(ROWS * COLS)) + r = clamp_int(int(self.selected_row.get()), 1, ROWS) - 1 + return list(range(r * COLS, r * COLS + COLS)) + + def _update_boxes(self): + if self._suspend: + return + if self.src_bgr is None: + return + + dx = float(self.offset_x.get()) + dy = float(self.offset_y.get()) + factor = float(self.factor.get()) + ang_deg = float(self.angle.get()) + ang = math.radians(ang_deg) + ca = math.cos(ang) + sa = math.sin(ang) + img_cx = self.src_w / 2.0 + img_cy = self.src_h / 2.0 + + centers = self._grid_centers() + half_w = CHAR_W / 2.0 + half_h = CHAR_H / 2.0 + half_w_s = half_w * factor + half_h_s = half_h * factor + sel = set(self._selected_indices()) + mode = str(self.mode.get()) + row = clamp_int(int(self.selected_row.get()), 1, ROWS) + + def tx(x: float, y: float): + vx = x - img_cx + vy = y - img_cy + rx = vx * ca - vy * sa + ry = vx * sa + vy * ca + return img_cx + factor * rx + dx, img_cy + factor * ry + dy + + for i, (cx, cy) in enumerate(centers): + ccx, ccy = tx(cx, cy) + p0x = ccx + (-half_w_s) * ca - (-half_h_s) * sa + p0y = ccy + (-half_w_s) * sa + (-half_h_s) * ca + p1x = ccx + (+half_w_s) * ca - (-half_h_s) * sa + p1y = ccy + (+half_w_s) * sa + (-half_h_s) * ca + p2x = ccx + (+half_w_s) * ca - (+half_h_s) * sa + p2y = ccy + (+half_w_s) * sa + (+half_h_s) * ca + p3x = ccx + (-half_w_s) * ca - (+half_h_s) * sa + p3y = ccy + (-half_w_s) * sa + (+half_h_s) * ca + + pts = [ + p0x * self.scale, + p0y * self.scale, + p1x * self.scale, + p1y * self.scale, + p2x * self.scale, + p2y * self.scale, + p3x * self.scale, + p3y * self.scale, + ] + self.canvas.coords(self._boxes[i], *pts) + if mode == "row" and i not in sel: + self.canvas.itemconfigure(self._boxes[i], state="hidden") + else: + self.canvas.itemconfigure(self._boxes[i], state="normal") + + stem = self.pages[self.page_index].stem + if mode == "all": + mode_hint = "25 框" + save_n = 25 + self.row_menu.config(state="disabled") + else: + mode_hint = f"单行 5 框(第 {row} 行)" + save_n = 5 + self.row_menu.config(state="normal") + self.btn_save.config(text=f"保存本页 {save_n} 个字") + self.status.config( + text=f"页:{self.page_index + 1}/{len(self.pages)}\n文件:{stem}\n模式:{mode_hint}\n偏移:({self.offset_x.get()}, {self.offset_y.get()})\n缩放:{self.factor.get():.3f}\n旋转:{self.angle.get():.1f}°\n框大小:{CHAR_W}×{CHAR_H}\n输出:{OUTPUT_FOLDER}" + ) + + self.btn_prev.config(state=tk.NORMAL if self.page_index > 0 else tk.DISABLED) + self.btn_next.config(state=tk.NORMAL if self.page_index < len(self.pages) - 1 else tk.DISABLED) + + def _on_arrow(self, e): + step = 10 if (e.state & 0x0001) else 1 + if e.keysym == "Left": + self.offset_x.set(clamp_int(self.offset_x.get() - step, OFFSET_MIN, OFFSET_MAX)) + elif e.keysym == "Right": + self.offset_x.set(clamp_int(self.offset_x.get() + step, OFFSET_MIN, OFFSET_MAX)) + elif e.keysym == "Up": + self.offset_y.set(clamp_int(self.offset_y.get() - step, OFFSET_MIN, OFFSET_MAX)) + elif e.keysym == "Down": + self.offset_y.set(clamp_int(self.offset_y.get() + step, OFFSET_MIN, OFFSET_MAX)) + self._update_boxes() + + def _on_mouse_down(self, e): + self._dragging = True + self._drag_start = (e.x, e.y, self.offset_x.get(), self.offset_y.get()) + + def _on_mouse_drag(self, e): + if not self._dragging or self._drag_start is None: + return + sx, sy, ox, oy = self._drag_start + if self.scale <= 0: + return + dx = int(round((e.x - sx) / self.scale)) + dy = int(round((e.y - sy) / self.scale)) + self.offset_x.set(clamp_int(ox + dx, OFFSET_MIN, OFFSET_MAX)) + self.offset_y.set(clamp_int(oy + dy, OFFSET_MIN, OFFSET_MAX)) + self._update_boxes() + + def _on_mouse_up(self, _e): + self._dragging = False + self._drag_start = None + + def reset_offset(self): + self.offset_x.set(0) + self.offset_y.set(0) + self.factor.set(1.0) + self.angle.set(0.0) + self._update_boxes() + + def prev_page(self): + if self.page_index > 0: + self._load_page(self.page_index - 1) + + def next_page(self): + if self.page_index < len(self.pages) - 1: + self._load_page(self.page_index + 1) + + def save_page(self): + if self.src_bgr is None: + return + + out_dir = Path(OUTPUT_FOLDER) + out_dir.mkdir(parents=True, exist_ok=True) + + sel_indices = self._selected_indices() + + dx = float(self.offset_x.get()) + dy = float(self.offset_y.get()) + factor = float(self.factor.get()) + ang_deg = float(self.angle.get()) + ang = math.radians(ang_deg) + ca = math.cos(ang) + sa = math.sin(ang) + img_cx = self.src_w / 2.0 + img_cy = self.src_h / 2.0 + + centers = self._grid_centers() + half_w = CHAR_W / 2.0 + half_h = CHAR_H / 2.0 + half_w_s = half_w * factor + half_h_s = half_h * factor + + def tx(x: float, y: float): + vx = x - img_cx + vy = y - img_cy + rx = vx * ca - vy * sa + ry = vx * sa + vy * ca + return img_cx + factor * rx + dx, img_cy + factor * ry + dy + + stem = self.pages[self.page_index].stem + for idx in sel_indices: + cx, cy = centers[idx] + r = idx // COLS + c = idx % COLS + ccx, ccy = tx(cx, cy) + + p0 = (ccx + (-half_w_s) * ca - (-half_h_s) * sa, ccy + (-half_w_s) * sa + (-half_h_s) * ca) + p1 = (ccx + (+half_w_s) * ca - (-half_h_s) * sa, ccy + (+half_w_s) * sa + (-half_h_s) * ca) + p2 = (ccx + (+half_w_s) * ca - (+half_h_s) * sa, ccy + (+half_w_s) * sa + (+half_h_s) * ca) + p3 = (ccx + (-half_w_s) * ca - (+half_h_s) * sa, ccy + (-half_w_s) * sa + (+half_h_s) * ca) + + src_pts = np.array([p0, p1, p2, p3], dtype=np.float32) + dst_pts = np.array( + [[0, 0], [CHAR_W - 1, 0], [CHAR_W - 1, CHAR_H - 1], [0, CHAR_H - 1]], + dtype=np.float32, + + ) + m = cv2.getPerspectiveTransform(src_pts, dst_pts) + crop = cv2.warpPerspective(self.src_bgr, m, (int(CHAR_W), int(CHAR_H)), flags=cv2.INTER_LINEAR, borderValue=(255, 255, 255)) + fname = f"{stem}_r{r + 1}_c{c + 1}.png" + imwrite_unicode(str(out_dir / fname), crop) + + self._set_notice(f"已保存 {stem}({len(sel_indices)} 个)", ok=True) + self._update_boxes() + + +if __name__ == "__main__": + root = tk.Tk() + PageCutterApp(root) + root.mainloop() diff --git a/labelme_cut.py b/labelme_cut.py new file mode 100644 index 0000000..f34f7d6 --- /dev/null +++ b/labelme_cut.py @@ -0,0 +1,63 @@ +import json +import cv2 +import os +from pathlib import Path + +# ==================== 你只需要改这 1 个路径 ==================== +LABEL_FOLDER = "篆文pic" # 放 json 和原图的文件夹(必须同目录) +OUTPUT_FOLDER = "chars" # 切割后输出的文件夹 +# =============================================================== + +Path(OUTPUT_FOLDER).mkdir(exist_ok=True) + +def imread_unicode(path): + return cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR) + +def imwrite_unicode(path, img): + cv2.imencode('.png', img)[1].tofile(path) + +def cut_labels(json_path): + # 读取标注 + with open(json_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + # 原图路径 + img_path = os.path.join(os.path.dirname(json_path), data['imagePath']) + img = imread_unicode(img_path) + if img is None: + print(f"找不到图片:{img_path}") + return + + # 文件名前缀 + prefix = Path(data['imagePath']).stem + + # 遍历每个框 + idx = 0 + for shape in data['shapes']: + points = shape['points'] + label = shape['label'] + + # 坐标转整数 + x1 = int(min(p[0] for p in points)) + y1 = int(min(p[1] for p in points)) + x2 = int(max(p[0] for p in points)) + y2 = int(max(p[1] for p in points)) + + # 切割 + crop = img[y1:y2, x1:x2] + + # 保存 + save_name = f"{prefix}_{label}_{idx:02d}.png" + save_path = os.path.join(OUTPUT_FOLDER, save_name) + imwrite_unicode(save_path, crop) + idx += 1 + + print(f"✅ 已切割:{img_path} → {idx} 个字") + +if __name__ == "__main__": + import numpy as np + json_files = list(Path(LABEL_FOLDER).glob("*.json")) + for jf in json_files: + cut_labels(jf) + + print("\n🎉 全部切割完成!输出在:", OUTPUT_FOLDER) diff --git a/list_dir.py b/list_dir.py new file mode 100644 index 0000000..761daaa --- /dev/null +++ b/list_dir.py @@ -0,0 +1,10 @@ +import os + +# 修改为你要扫描的目录 +target_dir = "J:\Python\ZhuanStudio\chars_gui" # Windows 也可以用 C:\\Users\\xxx\\Desktop + +# 列出所有文件,一行一个 +for filename in os.listdir(target_dir): + # 只输出文件,不输出文件夹 + if os.path.isfile(os.path.join(target_dir, filename)): + print(filename) diff --git a/main.py b/main.py new file mode 100644 index 0000000..eb389a0 --- /dev/null +++ b/main.py @@ -0,0 +1,16 @@ +# 这是一个示例 Python 脚本。 + +# 按 Shift+F10 执行或将其替换为您的代码。 +# 按 双击 Shift 在所有地方搜索类、文件、工具窗口、操作和设置。 + + +def print_hi(name): + # 在下面的代码行中使用断点来调试脚本。 + print(f'Hi, {name}') # 按 Ctrl+F8 切换断点。 + + +# 按装订区域中的绿色按钮以运行脚本。 +if __name__ == '__main__': + print_hi('PyCharm') + +# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助