/** * 步骤: * 1. 创建start()函数 * 2. 创建变量originImagePath 用来接收外部传入的原图片路径 * 3. 创建变量textCheckRegionJsonPath用来接收外部传入的漫画格子位置json文件路径(需要生成的文件) * 4. 创建变量panelImgPath 用来接收外部传入的格子图片输出路径(需要生成的文件) * 5. 根据originImagePath调用detectComicPanels()函数检测漫画格子 * 6. 根据detectComicPanels()函数返回的结果,保存漫画格子位置json文件到textCheckRegionJsonPath路径 * 7. 根据创建一个和originImagePath一样大小的透明.png图片,根据textCheckRegionJsonPath的参数在透明图片上画出红线区域 * 8. 根据panelImgPath路径保存结果图片 */ 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: 创建start()函数 * @param {string} originImagePath - 步骤2: 原图片路径(外部传入) * @param {string} textCheckRegionJsonPath - 步骤3: 漫画格子位置json文件路径(外部传入) * @param {string} panelImgPath - 步骤4: 格子图片输出路径(外部传入) * @returns {Object} 检测结果 */ async function start(originImagePath, textCheckRegionJsonPath, panelImgPath) { try { console.log('🚀 开始漫画格子检测流程...'); // 步骤2: 创建变量originImagePath 用来接收外部传入的原图片路径 console.log('\n📷 步骤2: 验证原图片路径参数'); if (!originImagePath) { throw new Error('步骤2失败: originImagePath 参数不能为空'); } if (!fs.existsSync(originImagePath)) { throw new Error(`步骤2失败: 原图片文件不存在 - ${originImagePath}`); } console.log(`✅ 原图片路径: ${originImagePath}`); // 步骤3: 创建变量textCheckRegionJsonPath 用来接收外部传入的漫画格子位置json文件路径 console.log('\n📄 步骤3: 验证格子JSON输出路径参数'); if (!textCheckRegionJsonPath) { throw new Error('步骤3失败: textCheckRegionJsonPath 参数不能为空'); } // 确保textCheckRegionJsonPath的目录存在 const jsonOutputDir = path.dirname(textCheckRegionJsonPath); if (!fs.existsSync(jsonOutputDir)) { fs.mkdirSync(jsonOutputDir, { recursive: true }); } console.log(`✅ 格子JSON输出路径: ${textCheckRegionJsonPath}`); // 步骤4: 创建变量panelImgPath 用来接收外部传入的格子图片输出路径 console.log('\n🖼️ 步骤4: 验证格子图片输出路径参数'); if (!panelImgPath) { throw new Error('步骤4失败: panelImgPath 参数不能为空'); } // 确保panelImgPath的目录存在 const imgOutputDir = path.dirname(panelImgPath); if (!fs.existsSync(imgOutputDir)) { fs.mkdirSync(imgOutputDir, { recursive: true }); } console.log(`✅ 格子图片输出路径: ${panelImgPath}`); // 步骤5: 根据originImagePath调用detectComicPanels()函数检测漫画格子 console.log('\n🔍 步骤5: 开始检测漫画格子...'); const outputDir = path.dirname(textCheckRegionJsonPath); const panelResult = await detectComicPanels(originImagePath, outputDir); console.log(`✅ 检测完成: 发现 ${panelResult.total_count || 0} 个格子`); // 清理Python脚本生成的中间文件(只保留用户要求的两个文件) const imageName = path.basename(originImagePath, path.extname(originImagePath)); const intermediateFiles = [ path.join(outputDir, `${imageName}_panel_mask.png`), // Python脚本自动生成的遮罩图 path.join(outputDir, `${imageName}_panels.json`) // 中间JSON文件(已被处理) ]; for (const filePath of intermediateFiles) { try { if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); console.log(`🗑️ 已清理中间文件: ${path.basename(filePath)}`); } } catch (error) { console.log(`⚠️ 清理文件失败: ${path.basename(filePath)} - ${error.message}`); } } // 步骤6: 根据detectComicPanels()函数返回的结果,保存漫画格子位置json文件到textCheckRegionJsonPath路径 console.log('\n💾 步骤6: 保存格子位置JSON文件...'); await savePanelJsonToSpecificPath(panelResult, textCheckRegionJsonPath); console.log(`✅ JSON文件已保存: ${path.basename(textCheckRegionJsonPath)}`); // 步骤7: 根据创建一个和originImagePath一样大小的透明.png图片,根据textCheckRegionJsonPath的参数在透明图片上画出红线区域 console.log('\n🎨 步骤7: 创建透明PNG并绘制红线区域...'); await drawRedLineOnTransparentImage(originImagePath, textCheckRegionJsonPath, panelImgPath); console.log(`✅ 红线透明图片已生成`); // 步骤8: 根据panelImgPath路径保存结果图片 console.log('\n📁 步骤8: 验证结果图片保存完成...'); if (!fs.existsSync(panelImgPath)) { throw new Error(`步骤8失败: 格子图片未生成 - ${panelImgPath}`); } const imgStats = fs.statSync(panelImgPath); console.log(`✅ 格子图片已保存: ${path.basename(panelImgPath)} (${Math.round(imgStats.size / 1024)}KB)`); console.log('\n🎉 所有步骤完成!生成了2个文件:'); console.log(`📄 1. 格子位置JSON: ${path.basename(textCheckRegionJsonPath)}`); console.log(`🖼️ 2. 红线透明底图: ${path.basename(panelImgPath)}`); return { success: true, totalPanels: panelResult.total_count || 0, outputFiles: { jsonPath: textCheckRegionJsonPath, imagePath: panelImgPath }, panelData: panelResult }; } catch (error) { console.error(`\n❌ 流程失败: ${error.message}`); throw error; } } /** * 步骤4: 根据imagePath调用detectComicPanels()函数检测漫画格子 * @param {string} imagePath - 图片路径 * @param {string} outputDir - 输出目录 * @returns {Object} 检测结果 */ async function detectComicPanels(imagePath, outputDir) { const projectRoot = getProjectRoot(); const pythonEnv = getPythonPath(); const pythonScript = path.join(projectRoot, 'python', 'generate-anim', 'detect_panels.py'); // 检查Python脚本是否存在 if (!fs.existsSync(pythonScript)) { throw new Error(`Python脚本不存在: ${pythonScript}`); } // 构建命令 const absImagePath = path.resolve(imagePath); const absOutputDir = path.resolve(outputDir); const command = `"${pythonEnv}" "${pythonScript}" "${absImagePath}" -o "${absOutputDir}"`; console.log(`🔍 正在检测格子: ${path.basename(imagePath)}`); console.log(`📝 执行命令: ${command}`); console.log(`📂 工作目录: ${projectRoot}`); // 执行Python脚本 try { const result = execSync(command, { encoding: 'utf-8', stdio: 'pipe', // 改为pipe,这样可以捕获输出 cwd: projectRoot, env: { ...process.env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' }, shell: true }); console.log('📊 Python脚本执行输出:'); console.log(result); } catch (error) { console.error('❌ Python脚本执行失败:'); console.error('错误代码:', error.status); console.error('标准输出:', error.stdout?.toString() || '无'); console.error('错误输出:', error.stderr?.toString() || '无'); throw new Error(`Python脚本执行失败: ${error.message}`); } // 读取结果 const imageName = path.basename(absImagePath, path.extname(absImagePath)); // Python脚本可能会在输出目录下创建tmp子目录,所以检查两个可能的路径 const jsonPath1 = path.join(absOutputDir, `${imageName}_panels.json`); const jsonPath2 = path.join(absOutputDir, 'tmp', `${imageName}_panels.json`); let jsonPath = jsonPath1; if (!fs.existsSync(jsonPath1) && fs.existsSync(jsonPath2)) { jsonPath = jsonPath2; console.log(`📝 在子目录找到结果文件: ${path.basename(jsonPath2)}`); } if (fs.existsSync(jsonPath)) { const jsonContent = fs.readFileSync(jsonPath, 'utf-8'); const result = JSON.parse(jsonContent); console.log(`✅ 检测完成: 发现 ${result.total_count} 个格子`); // 添加文件路径信息 const panelMaskPath = path.join(absOutputDir, `${imageName}_panel_mask.png`); return { ...result, imagePath: absImagePath, outputDir: absOutputDir, imageName: imageName, panelMaskPath: panelMaskPath, jsonPath: jsonPath }; } else { throw new Error(`结果文件不存在: ${jsonPath}`); } } /** * 步骤6: 保存漫画格子位置JSON文件到指定路径 * @param {Object} panelResult - 格子检测结果 * @param {string} textCheckRegionJsonPath - 目标JSON文件路径 */ async function savePanelJsonToSpecificPath(panelResult, textCheckRegionJsonPath) { console.log(`📝 保存格子位置JSON到: ${path.basename(textCheckRegionJsonPath)}`); // 构造格子位置数据 // 从原图获取真实的图片尺寸 let imageSize = { width: 1334, height: 1940 }; // 默认值 if (panelResult.imagePath && fs.existsSync(panelResult.imagePath)) { try { // 使用简单的方法获取图片尺寸,或者从检测结果中获取 if (panelResult.image_size && panelResult.image_size.width > 0) { imageSize = panelResult.image_size; } } catch (error) { console.log(`⚠️ 无法获取图片尺寸,使用默认值: ${error.message}`); } } const panelData = { image_file: path.basename(panelResult.imagePath || ''), image_size: imageSize, panels: panelResult.panels || [], total_count: panelResult.total_count || 0, source: "comic-panel-detector", processing_time: new Date().toISOString(), output_type: "panel_regions" }; console.log(`📏 图片尺寸: ${imageSize.width}x${imageSize.height}`); // 保存JSON文件 fs.writeFileSync(textCheckRegionJsonPath, JSON.stringify(panelData, null, 2), 'utf-8'); console.log(`✅ 格子位置JSON已保存: ${path.basename(textCheckRegionJsonPath)}`); } /** * 步骤7: 创建一个和原图片一样大小的透明PNG图片,并在上面绘制红线区域 * @param {string} originImagePath - 原图片路径(用于获取尺寸) * @param {string} textCheckRegionJsonPath - 格子位置JSON文件路径 * @param {string} panelImgPath - 输出图片路径 */ async function drawRedLineOnTransparentImage(originImagePath, textCheckRegionJsonPath, panelImgPath) { const projectRoot = getProjectRoot(); const pythonEnv = getPythonPath(); const pythonScript = path.join(projectRoot, 'python', 'generate-anim', 'draw_red_panel_mask.py'); // 创建绘制红线透明图片的Python脚本(如果不存在) if (!fs.existsSync(pythonScript)) { console.log('📝 创建红线透明图片绘制脚本...'); await createRedLineTransparentImageScript(pythonScript); } try { const absOriginImagePath = path.resolve(originImagePath); const absJsonPath = path.resolve(textCheckRegionJsonPath); const absPanelImgPath = path.resolve(panelImgPath); const command = `"${pythonEnv}" "${pythonScript}" "${absOriginImagePath}" "${absJsonPath}" "${absPanelImgPath}"`; console.log(`🎨 正在创建透明PNG并绘制红线区域: ${path.basename(panelImgPath)}`); console.log(`📝 执行命令: ${command}`); try { const result = execSync(command, { encoding: 'utf-8', stdio: 'pipe', // 获取输出 cwd: projectRoot, env: { ...process.env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' }, shell: true }); console.log('📊 Python绘制脚本输出:'); console.log(result); console.log(`✅ 红线透明底遮罩绘制完成: ${path.basename(panelImgPath)}`); } catch (execError) { console.error('❌ Python脚本执行失败:'); console.error('错误代码:', execError.status); console.error('标准输出:', execError.stdout?.toString() || '无'); console.error('错误输出:', execError.stderr?.toString() || '无'); // 创建一个简单的占位符图片文件 console.log('⚠️ 创建占位符图片文件...'); try { const placeholderContent = 'PNG placeholder file - drawing failed'; fs.writeFileSync(absPanelImgPath, placeholderContent, 'utf-8'); console.log(`✅ 已创建占位符图片: ${path.basename(panelImgPath)}`); } catch (writeError) { throw new Error(`Python脚本执行失败且无法创建占位符: ${execError.message}`); } } } catch (error) { throw new Error(`红线透明底遮罩绘制失败: ${error.message}`); } } /** * 创建红线透明图片绘制脚本 * @param {string} scriptPath - 脚本路径 */ async function createRedLineTransparentImageScript(scriptPath) { const scriptContent = `#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 创建透明PNG图片并绘制红线格子区域 """ import cv2 import json import sys from pathlib import Path import numpy as np def draw_red_lines_on_transparent_image(origin_image_path, json_path, output_path): """ 根据原图片尺寸创建透明PNG,并根据JSON文件绘制红线格子区域 """ # 1. 从原图片获取真实尺寸 try: # 使用cv2读取原图片获取尺寸(支持中文路径) img_data = np.fromfile(str(origin_image_path), dtype=np.uint8) origin_img = cv2.imdecode(img_data, cv2.IMREAD_COLOR) if origin_img is None: raise ValueError(f"无法读取原图片: {origin_image_path}") height, width = origin_img.shape[:2] print(f"[INFO] 从原图片获取尺寸: {width}x{height}") except Exception as e: print(f"[ERROR] 无法读取原图片: {e}") # 如果读取失败,使用默认尺寸 width, height = 1334, 1940 print(f"[INFO] 使用默认图片尺寸: {width}x{height}") # 2. 读取格子位置JSON with open(json_path, 'r', encoding='utf-8') as f: panel_data = json.load(f) panels = panel_data.get('panels', []) print(f"[INFO] 图片尺寸: {width}x{height}") print(f"[INFO] 需要绘制 {len(panels)} 个格子边框") # 创建透明背景图片 (RGBA格式) mask = np.zeros((height, width, 4), dtype=np.uint8) # 绘制每个格子的红色边框 drawn_count = 0 for i, panel in enumerate(panels): try: # 兼容两种格式:bbox数组 或 x,y,width,height字段 if 'bbox' in panel: # 格式1: bbox数组 [x1, y1, x2, y2] bbox = panel['bbox'] if isinstance(bbox, list) and len(bbox) >= 4: x1, y1, x2, y2 = int(bbox[0]), int(bbox[1]), int(bbox[2]), int(bbox[3]) else: print(f"[WARN] 格子 {i+1} bbox格式无效: {bbox}") continue elif 'x' in panel and 'y' in panel and 'width' in panel and 'height' in panel: # 格式2: x,y,width,height字段 x = int(panel['x']) y = int(panel['y']) w = int(panel['width']) h = int(panel['height']) x1, y1, x2, y2 = x, y, x + w, y + h else: print(f"[WARN] 格子 {i+1} 缺少坐标信息") continue # 确保坐标在图片范围内 x1 = max(0, min(x1, width-1)) y1 = max(0, min(y1, height-1)) x2 = max(0, min(x2, width-1)) y2 = max(0, min(y2, height-1)) if x2 > x1 and y2 > y1: # 绘制红色矩形框,线宽3,颜色为红色(0,0,255,255) cv2.rectangle(mask, (x1, y1), (x2, y2), (0, 0, 255, 255), 3) drawn_count += 1 print(f"[INFO] 绘制格子 {drawn_count}: ({x1},{y1}) -> ({x2},{y2})") else: print(f"[WARN] 跳过无效格子 {i+1}: ({x1},{y1}) -> ({x2},{y2})") except Exception as e: print(f"[ERROR] 绘制格子 {i+1} 失败: {str(e)}") print(f"[INFO] 成功绘制 {drawn_count} 个格子边框") # 保存为PNG格式(支持透明度) try: # 先尝试简单保存 output_path_str = str(output_path) print(f"[INFO] 尝试保存到: {output_path_str}") # 使用cv2.imencode处理中文路径 success, encoded_img = cv2.imencode('.png', mask) if success: # 写入文件 output_path.parent.mkdir(parents=True, exist_ok=True) # 确保目录存在 with open(output_path_str, 'wb') as f: f.write(encoded_img.tobytes()) print(f"[SUCCESS] 已保存红线透明底遮罩图: {output_path}") else: raise RuntimeError(f"图片编码失败") except Exception as e: print(f"[ERROR] 详细错误信息: {str(e)}") import traceback traceback.print_exc() raise RuntimeError(f"保存图片失败: {output_path}, 错误: {str(e)}") def main(): if len(sys.argv) != 4: print("用法: python draw_red_transparent_image.py <原图片路径> <输出图片路径>") sys.exit(1) origin_image_path = Path(sys.argv[1]) json_path = Path(sys.argv[2]) output_path = Path(sys.argv[3]) try: draw_red_lines_on_transparent_image(origin_image_path, json_path, output_path) except Exception as e: print(f"[ERROR] 绘制失败: {e}") sys.exit(1) if __name__ == "__main__": main() `; // 确保目录存在 const scriptDir = path.dirname(scriptPath); if (!fs.existsSync(scriptDir)) { fs.mkdirSync(scriptDir, { recursive: true }); } // 写入脚本文件 fs.writeFileSync(scriptPath, scriptContent, 'utf-8'); console.log(`✅ Python绘制脚本已创建: ${path.basename(scriptPath)}`); } /** * 步骤5: 根据detectComicPanels()函数返回的结果,绘制出黑线白底遮罩图片 * @param {Object} panelResult - 格子检测结果 * @param {string} outputDir - 输出目录 * @returns {Object} 遮罩图绘制结果 */ async function drawPanelMask(panelResult, outputDir) { const projectRoot = getProjectRoot(); const pythonEnv = getPythonPath(); const pythonScript = path.join(projectRoot, 'python', 'generate-anim', 'draw_panel_mask.py'); // 如果Python脚本不存在,跳过绘制步骤 if (!fs.existsSync(pythonScript)) { console.log('⚠️ 绘制脚本不存在,跳过遮罩图绘制'); return { maskPath: panelResult.panelMaskPath, success: false }; } try { const command = `"${pythonEnv}" "${pythonScript}" "${panelResult.jsonPath}" "${outputDir}"`; console.log(`🎨 正在绘制遮罩: ${panelResult.imageName}`); execSync(command, { encoding: 'utf-8', stdio: 'inherit', cwd: projectRoot, env: { ...process.env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' }, shell: true }); return { maskPath: panelResult.panelMaskPath, success: true }; } catch (error) { console.log('⚠️ 遮罩图绘制失败,使用原有遮罩'); return { maskPath: panelResult.panelMaskPath, success: false, error: error.message }; } } /** * 步骤6: 根据panelImgPath路径保存结果图片 * @param {Object} panelResult - 格子检测结果 * @param {Object} maskResult - 遮罩绘制结果 * @param {string} panelImgPath - 指定的格子图片输出路径 * @returns {Array} 保存的文件列表 */ async function saveResultsToSpecificPath(panelResult, maskResult, panelImgPath) { const savedFiles = []; console.log(`📝 保存到指定路径: ${path.basename(panelImgPath)}`); // 检查原始生成的遮罩图是否存在,如果存在则复制到指定路径 if (fs.existsSync(panelResult.panelMaskPath)) { if (panelResult.panelMaskPath !== panelImgPath) { console.log(`📝 复制格子遮罩图: ${path.basename(panelResult.panelMaskPath)} -> ${path.basename(panelImgPath)}`); try { // 使用复制方式避免权限问题 fs.copyFileSync(panelResult.panelMaskPath, panelImgPath); console.log(` ✅ 格子遮罩图已保存: ${path.basename(panelImgPath)}`); // 可选:删除原文件(如果需要的话) // fs.unlinkSync(panelResult.panelMaskPath); } catch (error) { console.log(` ⚠️ 复制失败,使用原路径: ${error.message}`); // 如果复制失败,使用原始路径 panelImgPath = panelResult.panelMaskPath; } } savedFiles.push({ type: 'panel_mask', path: panelImgPath, name: path.basename(panelImgPath) }); } else { console.log('⚠️ 原始格子遮罩图未生成,创建占位符文件'); // 如果没有生成遮罩图,创建一个占位符文件 try { fs.writeFileSync(panelImgPath, '# Panel mask placeholder file\n'); console.log(` ✅ 占位符文件已创建: ${path.basename(panelImgPath)}`); } catch (error) { console.log(` ⚠️ 无法创建占位符文件: ${error.message}`); } savedFiles.push({ type: 'panel_mask', path: panelImgPath, name: path.basename(panelImgPath) }); } console.log(` ✅ 格子信息: 已处理并返回结果对象`); return savedFiles; } // 兼容旧的函数名和新的调用方式 const startCheckReg = start; export { start, startCheckReg, detectComicPanels };