Spaces:
Runtime error
Runtime error
| 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}`); | |
| }); |