merge-image.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. /**
  2. * 步骤:
  3. * 1. 创建start()函数
  4. * 2. 创建变量textGreenPanelImgPath
  5. * 3. 创建变量panelImgPath
  6. * 4. 创建变量mergedImgPath 用来保存处理好的图片
  7. * 5. 将两张图片合并成一张图片(textGreenPanelImgPath在下,panelImgPath叠加在textGreenPanelImgPath上,透明区域要保留)
  8. * 6. 根据mergedImgPath路径保存结果图片(保存文件)
  9. */
  10. import fs from 'fs';
  11. import path from 'path';
  12. import { fileURLToPath } from 'url';
  13. import { execSync } from 'child_process';
  14. import { getPythonPath } from './python-path.js';
  15. const __filename = fileURLToPath(import.meta.url);
  16. const __dirname = path.dirname(__filename);
  17. /**
  18. * 获取项目根目录
  19. */
  20. function getProjectRoot() {
  21. return path.join(__dirname, '..');
  22. }
  23. async function startMergeImage(textGreenPanelImgPath, panelImgPath, mergedImgPath) {
  24. try {
  25. console.log('🚀 开始图片合并流程...');
  26. // 步骤2: 创建变量textGreenPanelImgPath 用来接收外部传入的绿色框图片路径
  27. console.log('\n📷 步骤2: 验证绿色框图片路径参数');
  28. if (!textGreenPanelImgPath) {
  29. throw new Error('步骤2失败: textGreenPanelImgPath 参数不能为空');
  30. }
  31. if (!fs.existsSync(textGreenPanelImgPath)) {
  32. throw new Error(`步骤2失败: 绿色框图片文件不存在 - ${textGreenPanelImgPath}`);
  33. }
  34. console.log(`✅ 绿色框图片路径: ${textGreenPanelImgPath}`);
  35. // 步骤3: 创建变量panelImgPath 用来接收外部传入的红线格子图片路径
  36. console.log('\n🔴 步骤3: 验证红线格子图片路径参数');
  37. if (!panelImgPath) {
  38. throw new Error('步骤3失败: panelImgPath 参数不能为空');
  39. }
  40. if (!fs.existsSync(panelImgPath)) {
  41. throw new Error(`步骤3失败: 红线格子图片文件不存在 - ${panelImgPath}`);
  42. }
  43. console.log(`✅ 红线格子图片路径: ${panelImgPath}`);
  44. // 步骤4: 创建变量mergedImgPath 用来保存处理好的图片
  45. console.log('\n📂 步骤4: 验证合并图片输出路径参数');
  46. if (!mergedImgPath) {
  47. throw new Error('步骤4失败: mergedImgPath 参数不能为空');
  48. }
  49. // 确保输出目录存在
  50. const outputDir = path.dirname(mergedImgPath);
  51. if (!fs.existsSync(outputDir)) {
  52. fs.mkdirSync(outputDir, { recursive: true });
  53. }
  54. console.log(`✅ 合并图片输出路径: ${mergedImgPath}`);
  55. // 步骤5: 将两张图片合并成一张图片(textGreenPanelImgPath在下,panelImgPath在上)
  56. console.log('\n🔗 步骤5: 开始合并两张图片...');
  57. console.log(' 📷 底层(背景): 带绿色文字识别框的图片');
  58. console.log(' 📷 顶层(叠加): 红线透明格子图片');
  59. console.log(' 🎯 合并方式: 红线透明图片叠加到绿色框图片上');
  60. await mergeImagesWithPython(textGreenPanelImgPath, panelImgPath, mergedImgPath);
  61. console.log(`✅ 图片合并完成`);
  62. // 步骤6: 根据mergedImgPath路径保存结果图片
  63. console.log('\n💾 步骤6: 验证结果图片保存完成...');
  64. if (!fs.existsSync(mergedImgPath)) {
  65. throw new Error(`步骤6失败: 合并后的图片未生成 - ${mergedImgPath}`);
  66. }
  67. const imgStats = fs.statSync(mergedImgPath);
  68. console.log(`✅ 合并图片已保存: ${path.basename(mergedImgPath)} (${Math.round(imgStats.size / 1024)}KB)`);
  69. console.log('\n🎉 所有步骤完成!');
  70. console.log(`📄 合并后的图片: ${path.basename(mergedImgPath)}`);
  71. return mergedImgPath;
  72. } catch (error) {
  73. console.error(`\n❌ 图片合并失败: ${error.message}`);
  74. if (error.stack) {
  75. console.error(error.stack);
  76. }
  77. throw error;
  78. }
  79. }
  80. /**
  81. * 使用Python脚本合并两张图片
  82. * @param {string} backgroundImgPath - 背景图片路径(底层)
  83. * @param {string} overlayImgPath - 叠加图片路径(顶层,透明PNG)
  84. * @param {string} outputImgPath - 输出图片路径
  85. */
  86. async function mergeImagesWithPython(backgroundImgPath, overlayImgPath, outputImgPath) {
  87. const projectRoot = getProjectRoot();
  88. const pythonEnv = getPythonPath();
  89. const pythonScript = path.join(projectRoot, 'python', 'generate-anim', 'merge_images.py');
  90. // 创建图片合并的Python脚本(如果不存在)
  91. if (!fs.existsSync(pythonScript)) {
  92. console.log('📝 创建图片合并Python脚本...');
  93. await createImageMergeScript(pythonScript);
  94. console.log(`✅ Python合并脚本已创建: ${path.basename(pythonScript)}`);
  95. }
  96. try {
  97. const absBackgroundPath = path.resolve(backgroundImgPath);
  98. const absOverlayPath = path.resolve(overlayImgPath);
  99. const absOutputPath = path.resolve(outputImgPath);
  100. const command = `"${pythonEnv}" "${pythonScript}" "${absBackgroundPath}" "${absOverlayPath}" "${absOutputPath}"`;
  101. console.log(`🔗 正在合并图片: ${path.basename(backgroundImgPath)} + ${path.basename(overlayImgPath)}`);
  102. console.log(`📝 执行命令: ${command}`);
  103. const result = execSync(command, {
  104. encoding: 'utf-8',
  105. stdio: 'pipe',
  106. cwd: projectRoot,
  107. env: {
  108. ...process.env,
  109. PYTHONIOENCODING: 'utf-8',
  110. PYTHONUTF8: '1'
  111. },
  112. shell: true
  113. });
  114. console.log('📊 Python合并脚本输出:');
  115. console.log(result);
  116. } catch (error) {
  117. console.error('❌ Python图片合并失败:');
  118. console.error('错误代码:', error.status);
  119. console.error('标准输出:', error.stdout?.toString() || '无');
  120. console.error('错误输出:', error.stderr?.toString() || '无');
  121. throw new Error(`Python图片合并失败: ${error.message}`);
  122. }
  123. }
  124. /**
  125. * 创建图片合并的Python脚本
  126. * @param {string} scriptPath - 脚本路径
  127. */
  128. async function createImageMergeScript(scriptPath) {
  129. const scriptContent = `#!/usr/bin/env python3
  130. # -*- coding: utf-8 -*-
  131. """
  132. 合并两张图片:将透明PNG图片叠加到背景图片上
  133. """
  134. import cv2
  135. import numpy as np
  136. import sys
  137. from pathlib import Path
  138. def merge_images(background_path, overlay_path, output_path):
  139. """
  140. 将透明PNG图片叠加到背景图片上
  141. 参数:
  142. background_path: 背景图片路径(底层)
  143. overlay_path: 叠加图片路径(顶层,支持透明通道)
  144. output_path: 输出图片路径
  145. """
  146. try:
  147. # 读取背景图片(支持中文路径)
  148. bg_data = np.fromfile(str(background_path), dtype=np.uint8)
  149. background = cv2.imdecode(bg_data, cv2.IMREAD_COLOR)
  150. if background is None:
  151. raise ValueError(f"无法读取背景图片: {background_path}")
  152. print(f"[INFO] 背景图片尺寸: {background.shape[1]}x{background.shape[0]}")
  153. # 读取叠加图片(支持透明通道)
  154. overlay_data = np.fromfile(str(overlay_path), dtype=np.uint8)
  155. overlay = cv2.imdecode(overlay_data, cv2.IMREAD_UNCHANGED)
  156. if overlay is None:
  157. raise ValueError(f"无法读取叠加图片: {overlay_path}")
  158. print(f"[INFO] 叠加图片尺寸: {overlay.shape[1]}x{overlay.shape[0]}")
  159. print(f"[INFO] 叠加图片通道数: {overlay.shape[2] if len(overlay.shape) > 2 else 1}")
  160. # 确保两张图片尺寸一致
  161. if background.shape[:2] != overlay.shape[:2]:
  162. print(f"[WARN] 图片尺寸不匹配,调整叠加图片尺寸")
  163. overlay = cv2.resize(overlay, (background.shape[1], background.shape[0]))
  164. # 处理叠加图片的透明通道
  165. if len(overlay.shape) == 3 and overlay.shape[2] == 4:
  166. # RGBA格式,有透明通道
  167. print(f"[INFO] 处理RGBA透明图片")
  168. # 分离RGB和Alpha通道
  169. overlay_rgb = overlay[:, :, :3]
  170. alpha = overlay[:, :, 3] / 255.0 # 归一化到0-1
  171. # 扩展alpha通道到3个维度
  172. alpha_3ch = np.stack([alpha, alpha, alpha], axis=2)
  173. # Alpha混合
  174. result = background * (1 - alpha_3ch) + overlay_rgb * alpha_3ch
  175. result = result.astype(np.uint8)
  176. elif len(overlay.shape) == 3 and overlay.shape[2] == 3:
  177. # RGB格式,没有透明通道,直接叠加非白色区域
  178. print(f"[INFO] 处理RGB图片,将白色区域视为透明")
  179. # 创建mask:白色区域(255,255,255)为透明,其他为不透明
  180. white_mask = np.all(overlay == [255, 255, 255], axis=2)
  181. alpha = (~white_mask).astype(float) # 非白色区域alpha=1
  182. # 扩展alpha到3个维度
  183. alpha_3ch = np.stack([alpha, alpha, alpha], axis=2)
  184. # Alpha混合
  185. result = background * (1 - alpha_3ch) + overlay * alpha_3ch
  186. result = result.astype(np.uint8)
  187. else:
  188. # 灰度图或其他格式,简单叠加
  189. print(f"[INFO] 处理其他格式图片")
  190. if len(overlay.shape) == 2:
  191. overlay = cv2.cvtColor(overlay, cv2.COLOR_GRAY2BGR)
  192. result = cv2.addWeighted(background, 0.7, overlay, 0.3, 0)
  193. print(f"[INFO] 合并完成,结果图片尺寸: {result.shape[1]}x{result.shape[0]}")
  194. # 保存结果图片(支持中文路径)
  195. success, encoded_img = cv2.imencode('.png', result)
  196. if success:
  197. output_path.parent.mkdir(parents=True, exist_ok=True)
  198. with open(str(output_path), 'wb') as f:
  199. f.write(encoded_img.tobytes())
  200. print(f"[SUCCESS] 已保存合并图片: {output_path}")
  201. else:
  202. raise RuntimeError(f"图片编码失败: {output_path}")
  203. except Exception as e:
  204. print(f"[ERROR] 详细错误信息: {str(e)}")
  205. import traceback
  206. traceback.print_exc()
  207. raise RuntimeError(f"合并图片失败: {str(e)}")
  208. def main():
  209. if len(sys.argv) != 4:
  210. print("用法: python merge_images.py <背景图片路径> <叠加图片路径> <输出图片路径>")
  211. sys.exit(1)
  212. background_path = Path(sys.argv[1])
  213. overlay_path = Path(sys.argv[2])
  214. output_path = Path(sys.argv[3])
  215. try:
  216. merge_images(background_path, overlay_path, output_path)
  217. except Exception as e:
  218. print(f"[ERROR] 合并失败: {e}")
  219. sys.exit(1)
  220. if __name__ == "__main__":
  221. main()
  222. `;
  223. // 确保目录存在
  224. const scriptDir = path.dirname(scriptPath);
  225. if (!fs.existsSync(scriptDir)) {
  226. fs.mkdirSync(scriptDir, { recursive: true });
  227. }
  228. // 写入脚本文件
  229. fs.writeFileSync(scriptPath, scriptContent, 'utf-8');
  230. }
  231. /**
  232. * 导出函数供外部调用
  233. */
  234. export { startMergeImage };