const express = require('express'); const cv = require('@u4/opencv4nodejs'); const Jimp = require('jimp'); const axios = require('axios'); const fs = require('fs'); const os = require('os'); const path = require('path'); const ffmpeg = require('fluent-ffmpeg'); const serveStatic = require('serve-static'); const app = express(); const PORT = 7860; // 从文件夹加载所有分类器 async function loadClassifiersFromFolder(folderPath) { console.log(`开始从文件夹 ${folderPath} 加载分类器...`); const classifiers = []; const files = fs.readdirSync(folderPath); for (const file of files) { const filePath = path.join(folderPath, file); const classifier = new cv.CascadeClassifier(filePath); classifiers.push(classifier); console.log(`加载分类器: ${file}`); } console.log('分类器加载完成'); return classifiers; } // GIF 转换为视频 async function gifToVideo(inputGifPath, outputVideoPath) { console.log('开始将GIF转换为视频...'); return new Promise((resolve, reject) => { ffmpeg(inputGifPath) .inputFormat('gif') .output(outputVideoPath) .on('start', (commandLine) => { console.log('FFmpeg 命令:', commandLine); }) .on('end', () => { console.log('GIF成功转换为视频:', outputVideoPath); resolve(); }) .on('error', (err) => { console.error('GIF转换为视频时出错:', err); reject(err); }) .run(); }); } // 视频转换为 GIF async function videoToGif(inputVideoPath, outputGifPath) { console.log('开始将视频转换为GIF...'); return new Promise((resolve, reject) => { ffmpeg(inputVideoPath) .output(outputGifPath) .on('start', (commandLine) => { console.log('FFmpeg 命令:', commandLine); }) .on('end', () => { console.log('视频成功转换为GIF:', outputGifPath); resolve(); }) .on('error', (err) => { console.error('视频转换为GIF时出错:', err); reject(err); }) .run(); }); } // 处理图片的 Express 接口 app.get('/process-image', async (req, res) => { try { const { imageUrl, replacementImageUrl } = req.query; const tempDir = os.tmpdir(); const tempImagePath = path.join(tempDir, 'temp_image.jpg'); const tempImagePaths = path.join(tempDir, 'temp_images.jpg'); const outputImagePath = path.join(tempDir, 'output_image.jpg'); const classifiersFolder = path.join(__dirname, 'classifiers'); console.log('接收到图片处理请求:', imageUrl); // 下载图片到本地 console.log('开始下载图片...'); const response = await axios.get(imageUrl, { responseType: 'stream' }); const writer = fs.createWriteStream(tempImagePath); response.data.pipe(writer); // 等待文件写入完成 await new Promise((resolve, reject) => { writer.on('finish', resolve); writer.on('error', reject); }); let replacementImageBuffer; if (replacementImageUrl === 'replace') { // 使用默认的本地替换图像路径 const defaultReplacementImagePath = './replacement_face.png'; replacementImageBuffer = await Jimp.read(defaultReplacementImagePath); } else { // 下载并转换替换图片到内存中 const response = await axios.get(replacementImageUrl, { responseType: 'stream' }); const writer = fs.createWriteStream(tempImagePaths); response.data.pipe(writer); // 等待文件写入完成 await new Promise((resolve, reject) => { writer.on('finish', resolve); writer.on('error', reject); }); replacementImageBuffer = await Jimp.read(tempImagePaths) } console.log('图片下载完成:', tempImagePath); // 从文件夹动态加载分类器 const classifiers = await loadClassifiersFromFolder(classifiersFolder); // 读取图片 console.log('开始读取图片...'); const image = await cv.imreadAsync(tempImagePath); const grayImage = image.bgrToGray(); // 检测人脸 console.log('开始检测人脸...'); const faces = []; classifiers.forEach(classifier => { const allFaces = classifier.detectMultiScale(grayImage).objects; faces.push(...allFaces); }); const replacementFace = replacementImageBuffer; if (Array.isArray(faces) && faces.length > 0) { console.log(`检测到 ${faces.length} 张脸`); for (const rect of faces) { const resizedSubstituteImage = await replacementFace.clone().resize(rect.width, rect.height); const substituteRegion = image.getRegion(rect); for (let y = 0; y < rect.height; y++) { for (let x = 0; x < rect.width; x++) { const { r, g, b, a } = Jimp.intToRGBA(resizedSubstituteImage.getPixelColor(x, y)); if (a > 0) { const color = new cv.Vec3(b, g, r); substituteRegion.set(y, x, color); } } } } } else { console.error('未检测到人脸或faces数组为空'); throw new Error('No faces detected in the image.'); } // 保存处理后的图片 await cv.imwriteAsync(outputImagePath, image); console.log('图片处理完成并保存:', outputImagePath); // 发送处理后的图片 res.set('Content-Type', 'image/jpeg'); res.sendFile(outputImagePath, async (err) => { if (err) { console.error('发送文件时出错:', err); res.status(500).send('内部服务器错误'); } else { // 删除临时文件 fs.unlinkSync(tempImagePath); if (fs.existsSync(tempImagePaths)) { fs.unlinkSync(tempImagePaths); } fs.unlinkSync(outputImagePath); } }); } catch (error) { console.error('处理图片时出错:', error); res.status(500).send('内部服务器错误'); } }); // 处理 GIF 的 Express 接口 app.get('/processGif', async (req, res) => { try { const { gifUrl, replacementImageUrl } = req.query; const tempDir = os.tmpdir(); const tempGifPath = path.join(tempDir, 'temp.gif'); const tempGifPaths = path.join(tempDir, 'temps.gif'); const tempVideoPath = path.join(tempDir, 'temp_video.mp4'); const outputGifPath = path.join(tempDir, 'output.gif'); const classifiersFolder = path.join(__dirname, 'classifiers'); console.log('接收到GIF处理请求:', gifUrl); // 下载 GIF 到本地 console.log('开始下载GIF...'); const response = await axios.get(gifUrl, { responseType: 'stream' }); const writer = fs.createWriteStream(tempGifPath); response.data.pipe(writer); // 等待文件写入完成 await new Promise((resolve, reject) => { writer.on('finish', resolve); writer.on('error', reject); }); let replacementImageBuffer = tempGifPaths if (replacementImageUrl === 'replace') { // 使用默认的本地替换图像路径 const defaultReplacementImagePath = './replacement_face.png'; replacementImageBuffer = await Jimp.read(defaultReplacementImagePath); } else { // 下载并转换替换图片到内存中 const response = await axios.get(replacementImageUrl, { responseType: 'stream' }); const writer = fs.createWriteStream(tempGifPaths); response.data.pipe(writer); // 等待文件写入完成 await new Promise((resolve, reject) => { writer.on('finish', resolve); writer.on('error', reject); }); replacementImageBuffer = await Jimp.read(tempGifPaths) } console.log('GIF下载完成:', tempGifPath); // 从文件夹动态加载分类器 const classifiers = await loadClassifiersFromFolder(classifiersFolder); // 将GIF转换为视频 await gifToVideo(tempGifPath, tempVideoPath); // 读取视频文件 console.log('开始读取视频文件...'); const videoCapture = new cv.VideoCapture(tempVideoPath); const frameWidth = videoCapture.get(cv.CAP_PROP_FRAME_WIDTH); const frameHeight = videoCapture.get(cv.CAP_PROP_FRAME_HEIGHT); const originalFps = videoCapture.get(cv.CAP_PROP_FPS); const videoWriter = new cv.VideoWriter(path.join(tempDir, 'out.mp4'), cv.VideoWriter.fourcc('avc1'), originalFps, new cv.Size(frameWidth, frameHeight)); console.log('视频文件读取完成'); // 处理每一帧 console.log('开始处理每一帧...'); while (true) { const frame = videoCapture.read(); if (frame.empty) break; const grayFrame = frame.bgrToGray(); const faces = []; classifiers.forEach(classifier => { const allfaces = classifier.detectMultiScale(grayFrame).objects; faces.push(...allfaces); }); const replacementFace = replacementImageBuffer; if (Array.isArray(faces) && faces.length > 0) { console.log(`检测到 ${faces.length} 张脸`); for (const rect of faces) { const resizedSubstituteImage = await replacementFace.clone().resize(rect.width, rect.height); const substituteRegion = frame.getRegion(rect); for (let y = 0; y < rect.height; y++) { for (let x = 0; x < rect.width; x++) { const { r, g, b, a } = Jimp.intToRGBA(resizedSubstituteImage.getPixelColor(x, y)); if (a > 0) { const color = new cv.Vec3(b, g, r); substituteRegion.set(y, x, color); } } } } } else { console.error('未检测到人脸或faces数组为空'); throw new Error('No faces detected in the image.'); } videoWriter.write(frame); } videoCapture.release(); videoWriter.release(); cv.destroyAllWindows(); console.log('帧处理完成'); await videoToGif(path.join(tempDir, 'out.mp4'), outputGifPath); res.set('Content-Type', 'image/gif'); res.sendFile(outputGifPath, async (err) => { if (err) { console.error('发送GIF文件时出错:', err); res.status(500).send('内部服务器错误'); } else { // 删除临时文件 fs.unlinkSync(tempGifPath); if (fs.existsSync(tempGifPaths)) { fs.unlinkSync(tempGifPaths); } fs.unlinkSync(tempVideoPath); fs.unlinkSync(outputGifPath); } }); } catch (error) { console.error('处理GIF时出错:', error); res.status(500).send('内部服务器错误'); } }); const apiStats = { '/process-image': 0, '/processGif': 0 }; // 中间件记录API请求次数 app.use((req, res, next) => { if (apiStats[req.path] !== undefined) { apiStats[req.path]++; } next(); }); // 提供API请求统计数据的接口 app.get('/api-stats', (req, res) => { res.json(apiStats); }); // 每天重置统计数据 setInterval(() => { for (const api in apiStats) { apiStats[api] = 0; } }, 24 * 60 * 60 * 1000); // 24小时 // 每月重置统计数据 const resetStatsMonthly = () => { const now = new Date(); const firstDayOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1); const timeUntilNextMonth = firstDayOfNextMonth - now; setTimeout(() => { for (const api in apiStats) { apiStats[api] = 0; } resetStatsMonthly(); }, timeUntilNextMonth); }; resetStatsMonthly(); app.use(serveStatic(__dirname)); // 添加这一行,设置静态文件服务器 app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'index.html')); // 修改这一行,发送HTML文件 }); // 启动 Express 服务器 app.listen(PORT, () => { console.log(`服务器运行在端口 ${PORT}`); });