| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- /**
- * 步骤1: 创建startCutDialogBlock()函数
- * 步骤2: 接收参数:cutPanelImgDir (切割好的图片存放路径)
- * 步骤3: 接收参数:cutCheckResultArr (切割好的图片路径数组,根据这个路径的图片文件名,新建对应数量的文件夹)
- * 步骤3: 调用Python脚本依次检测绿色线框并切图
- * 步骤4: 给切割好的图片编号,根据从右到左,从上到下顺序编号,保存到cutCheckResultArr对应图片文件名的文件夹下
- * 步骤5: 返回切割好的图片路径数组
- */
- import fs from 'fs';
- import path from 'path';
- import { fileURLToPath } from 'url';
- import { execSync } from 'child_process';
- import { getPythonPath } from './python-path.js';
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = path.dirname(__filename);
- function getProjectRoot() {
- return path.join(__dirname, '..');
- }
- /**
- * 步骤1: 创建startCutDialogBlock()函数
- * @param {string} cutPanelImgDir - 步骤2: 切割好的图片存放路径(外部传入)
- * @param {Array} cutCheckResultArr - 步骤3: 切割好的图片路径数组(外部传入)
- * @returns {Promise<Array>} 步骤5: 返回切割好的图片路径数组
- */
- async function startCutDialogBlock(cutPanelImgDir, cutCheckResultArr) {
- try {
- console.log('🚀 开始文字块切割流程...');
-
- // 步骤2: 接收参数:cutPanelImgDir (切割好的图片存放路径)
- console.log('\n📂 步骤2: 验证图片存放路径参数');
- if (!cutPanelImgDir) {
- throw new Error('步骤2失败: cutPanelImgDir 参数不能为空');
- }
- if (!fs.existsSync(cutPanelImgDir)) {
- throw new Error(`步骤2失败: 图片存放路径不存在 - ${cutPanelImgDir}`);
- }
- console.log(`✅ 图片存放路径: ${cutPanelImgDir}`);
-
- // 步骤3: 接收参数:cutCheckResultArr (切割好的图片路径数组)
- console.log('\n📋 步骤3: 验证图片路径数组参数');
- if (!cutCheckResultArr || !Array.isArray(cutCheckResultArr)) {
- throw new Error('步骤3失败: cutCheckResultArr 必须是一个数组');
- }
- if (cutCheckResultArr.length === 0) {
- throw new Error('步骤3失败: cutCheckResultArr 不能为空数组');
- }
- console.log(`✅ 图片路径数组长度: ${cutCheckResultArr.length}`);
-
- // 根据图片路径的文件名,新建对应数量的文件夹
- console.log('\n📁 步骤3: 为每个图片创建对应的文件夹...');
- const panelFolders = [];
- for (let i = 0; i < cutCheckResultArr.length; i++) {
- const panelImgPath = cutCheckResultArr[i];
- const panelFileName = path.basename(panelImgPath, path.extname(panelImgPath));
- const panelFolderPath = path.join(cutPanelImgDir, panelFileName);
-
- if (!fs.existsSync(panelFolderPath)) {
- fs.mkdirSync(panelFolderPath, { recursive: true });
- }
-
- panelFolders.push({
- panelImgPath: panelImgPath,
- panelFolderPath: panelFolderPath,
- panelFileName: panelFileName
- });
-
- console.log(` 📁 [${i + 1}] 创建文件夹: ${panelFileName}/`);
- }
-
- // 步骤3: 调用Python脚本依次检测绿色线框并切图
- console.log('\n🔍 步骤3: 调用Python脚本检测绿色线框...');
- const allCutFiles = [];
-
- for (let i = 0; i < panelFolders.length; i++) {
- const { panelImgPath, panelFolderPath, panelFileName } = panelFolders[i];
-
- console.log(`\n 🔍 [${i + 1}/${panelFolders.length}] 处理图片: ${panelFileName}`);
-
- const cutFiles = await cutDialogBlocks(panelImgPath, panelFolderPath);
-
- // 步骤4: 给切割好的图片编号,根据从右到左,从上到下顺序编号
- console.log(` ✅ 检测到 ${cutFiles.length} 个文字块,正在编号保存...`);
-
- allCutFiles.push(...cutFiles);
- }
-
- // 步骤5: 返回切割好的图片路径数组
- console.log('\n📋 步骤5: 准备返回文字块图片路径数组...');
- console.log(`📄 文字块图片路径列表 (${allCutFiles.length} 个):`);
- allCutFiles.forEach((file, index) => {
- console.log(` ${index + 1}. ${path.basename(file)}`);
- });
- console.log(`✅ 步骤5完成: 文字块图片路径数组已准备就绪 (${allCutFiles.length} 个路径)`);
-
- console.log('\n🎉 所有步骤完成!');
- console.log(`📊 共处理 ${panelFolders.length} 个格子图片`);
- console.log(`📊 共切割 ${allCutFiles.length} 个文字块图片`);
-
- return allCutFiles; // 步骤5: 返回切割好的图片路径数组
-
- } catch (error) {
- console.error(`\n❌ 文字块切割失败: ${error.message}`);
- throw error;
- }
- }
- /**
- * 处理单个格子图片,检测绿色线框并切割文字块
- * @param {string} panelImgPath - 格子图片路径
- * @param {string} outputFolderPath - 输出文件夹路径
- * @returns {Promise<Array>} 切割后的文件路径数组
- */
- async function cutDialogBlocks(panelImgPath, outputFolderPath) {
- const projectRoot = getProjectRoot();
- const pythonEnv = getPythonPath();
-
- // 使用绿色线框检测脚本
- const cutScript = path.join(projectRoot, 'python', 'generate-anim', 'cut_dialog_blocks_by_green_box.py');
- if (!fs.existsSync(cutScript)) {
- throw new Error(`Python脚本不存在: ${cutScript}`);
- }
-
- // 调用Python脚本
- const command = `"${pythonEnv}" "${cutScript}" "${panelImgPath}" "${outputFolderPath}"`;
-
- try {
- const output = execSync(command, {
- encoding: 'utf-8',
- stdio: 'pipe',
- cwd: projectRoot,
- env: {
- ...process.env,
- PYTHONIOENCODING: 'utf-8',
- PYTHONUTF8: '1'
- },
- shell: true
- });
- // 打印Python脚本的调试输出(完整输出)
- if (output) {
- const debugLines = output.split('\n').filter(line => line.includes('[DEBUG]') || line.includes('center_x'));
- if (debugLines.length > 0) {
- console.log(` ${debugLines.join('\n ')}`);
- }
- }
- } catch (error) {
- console.log(` ⚠️ 该图片未检测到文字块: ${path.basename(panelImgPath)}`);
- if (error.stdout) {
- const debugLines = error.stdout.split('\n').filter(line => line.includes('[DEBUG]') || line.includes('center_x'));
- if (debugLines.length > 0) {
- console.log(` ${debugLines.join('\n ')}`);
- }
- }
- return [];
- }
-
- // 获取切割结果文件列表(按从右到左、从上到下的顺序编号)
- const cutFiles = fs.readdirSync(outputFolderPath)
- .filter(f => f.match(/^dialog_\d+\.png$/)) // 匹配 dialog_1.png, dialog_2.png 等
- .sort((a, b) => {
- const numA = parseInt(a.match(/dialog_(\d+)/)?.[1] || '0');
- const numB = parseInt(b.match(/dialog_(\d+)/)?.[1] || '0');
- return numA - numB;
- })
- .map(f => path.join(outputFolderPath, f));
-
- return cutFiles;
- }
- /**
- * 创建绿色线框检测和切割的Python脚本
- */
- function createCutDialogScript(scriptPath) {
- const scriptContent = `#!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- 简单的绿色线框检测和文字块切割脚本
- """
- import cv2
- import numpy as np
- import sys
- from pathlib import Path
- def detect_green_boxes_and_cut(image_path, output_dir):
- """检测绿色线框并切割文字块"""
- # 读取图片
- img_data = np.fromfile(str(image_path), dtype=np.uint8)
- img = cv2.imdecode(img_data, cv2.IMREAD_COLOR)
- if img is None:
- raise ValueError(f"无法读取图片: {image_path}")
-
- print(f"[INFO] 图片尺寸: {img.shape[1]}x{img.shape[0]}")
-
- # 检测绿色线框
- # 绿色范围 (BGR格式)
- lower_green = np.array([0, 100, 0])
- upper_green = np.array([100, 255, 100])
-
- # 创建绿色掩码
- mask = cv2.inRange(img, lower_green, upper_green)
-
- # 寻找轮廓
- contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
-
- if len(contours) == 0:
- print("[INFO] 未检测到绿色线框")
- return
-
- # 获取边界矩形
- boxes = []
- for contour in contours:
- x, y, w, h = cv2.boundingRect(contour)
- if w > 10 and h > 10: # 过滤小框
- boxes.append({'x': x, 'y': y, 'w': w, 'h': h})
-
- if len(boxes) == 0:
- print("[INFO] 未找到有效的文字框")
- return
-
- print(f"[INFO] 检测到 {len(boxes)} 个绿色文字框")
-
- # 按从右到左、从上到下排序
- boxes.sort(key=lambda b: (b['y'], -b['x']))
-
- # 确保输出目录存在
- Path(output_dir).mkdir(parents=True, exist_ok=True)
-
- # 切割并保存每个文字块
- for i, box in enumerate(boxes, 1):
- x, y, w, h = box['x'], box['y'], box['w'], box['h']
-
- # 添加少量边距
- padding = 5
- x = max(0, x - padding)
- y = max(0, y - padding)
- w = min(w + 2 * padding, img.shape[1] - x)
- h = min(h + 2 * padding, img.shape[0] - y)
-
- # 切割区域
- dialog_img = img[y:y+h, x:x+w]
-
- # 保存文件
- output_file = Path(output_dir) / f"dialog_{i}.png"
- success, encoded = cv2.imencode('.png', dialog_img)
- if success:
- encoded.tofile(str(output_file))
- print(f"[{i}/{len(boxes)}] 保存: {output_file.name} ({w}x{h})")
-
- print(f"✅ 文字块切割完成: {len(boxes)} 个")
- if __name__ == '__main__':
- if len(sys.argv) != 3:
- print("用法: python cut_dialog_blocks.py <图片路径> <输出目录>")
- sys.exit(1)
-
- try:
- detect_green_boxes_and_cut(sys.argv[1], sys.argv[2])
- except Exception as e:
- print(f"[ERROR] {e}")
- sys.exit(1)
- `;
- // 确保目录存在
- const scriptDir = path.dirname(scriptPath);
- if (!fs.existsSync(scriptDir)) {
- fs.mkdirSync(scriptDir, { recursive: true });
- }
-
- fs.writeFileSync(scriptPath, scriptContent, 'utf-8');
- }
- /**
- * 导出函数供外部调用
- */
- export { startCutDialogBlock };
|