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("✅ 全部自动切割+自动边界检测定位完成!")