| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575 |
- /**
- * 步骤:
- * 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 <原图片路径> <JSON文件路径> <输出图片路径>")
- 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 };
|