/** * AI 智能抠图与消除 - 前端交互、剪贴板贴图与后端 POST 核心脚本 * 支持:桌面/移动端触控、魔法棒智能点选、画笔微调、Ctrl+V 贴图上传、异步 POST 后端 AI 接口 */ // DOM 元素获取 const fileInput = document.getElementById('file'); const dropZone = document.getElementById('drop-zone'); const modeButtons = document.querySelectorAll('.modes button'); const toolButtons = document.querySelectorAll('.tool-btn'); const sizeSlider = document.getElementById('size'); const brushSizeVal = document.getElementById('brush-size-val'); const runButton = document.getElementById('run'); const resetButton = document.getElementById('reset'); const statusDiv = document.getElementById('status'); const canvas = document.getElementById('canvas'); const overlay = document.getElementById('overlay'); const loadingMask = document.getElementById('loading'); const resultImg = document.getElementById('result'); const placeholderTip = document.getElementById('placeholder-tip'); const downloadBtn = document.getElementById('download'); const ctx = canvas.getContext('2d', { willReadFrequently: true }); const overlayCtx = overlay.getContext('2d', { willReadFrequently: true }); // 应用状态管理 const state = { image: null, // 当前加载的原始图片对象 mode: 'cutout', // 当前模式: 'cutout' (抠出) | 'inpaint' (消除) tool: 'select', // 当前工具: 'select' (智能点选) | 'add' (增加区域) | 'erase' (擦除) brushSize: 35, // 画笔半径 isDrawing: false, // 是否正在绘制/涂抹 imgWidth: 0, // 图片实际宽度 imgHeight: 0, // 图片实际高度 maskCanvas: null, // 用于存储用户选区/涂抹遮罩的离屏 Canvas maskCtx: null }; // 初始化系统 function init() { bindEvents(); updateStatus('请上传图片,或直接在页面按 Ctrl+V 粘贴截图'); } // 绑定所有交互事件 function bindEvents() { // 文件选择与拖拽上传 fileInput.addEventListener('change', handleFileSelect); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.style.borderColor = 'var(--primary)'; }); dropZone.addEventListener('dragleave', () => { dropZone.style.borderColor = ''; }); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.style.borderColor = ''; if (e.dataTransfer.files && e.dataTransfer.files[0]) { fileInput.files = e.dataTransfer.files; handleFileSelect({ target: fileInput }); } }); // 核心新增:全局剪贴板粘贴监听(支持截图直接 Ctrl + V 贴图) window.addEventListener('paste', (e) => { const items = (e.clipboardData || e.originalEvent.clipboardData).items; for (let item of items) { if (item.type.indexOf('image') === 0) { const blob = item.getAsFile(); const fakeEvent = { target: { files: [blob] } }; handleFileSelect(fakeEvent); updateStatus('已通过剪贴板粘贴加载图片!'); break; } } }); // 模式切换 (抠出对象 vs 删除并补全) modeButtons.forEach(btn => { btn.addEventListener('click', () => { modeButtons.forEach(b => b.classList.remove('active')); btn.classList.add('active'); state.mode = btn.dataset.mode; updateStatus(`已切换至: ${state.mode === 'cutout' ? '抠出对象模式' : '删除并补全模式'}`); }); }); // 工具切换 (点选 / 增加 / 擦除) toolButtons.forEach(btn => { btn.addEventListener('click', () => { toolButtons.forEach(b => b.classList.remove('active')); btn.classList.add('active'); state.tool = btn.id; const toolNames = { select: '点击智能选择', add: '画笔增加区域', erase: '画笔擦除区域' }; updateStatus(`当前工具: ${toolNames[state.tool]}`); }); }); // 画笔大小滑块 sizeSlider.addEventListener('input', (e) => { state.brushSize = parseInt(e.target.value); brushSizeVal.textContent = state.brushSize; }); // 画布指针事件 overlay.addEventListener('pointerdown', handlePointerDown); overlay.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', handlePointerUp); // 重置与运行处理 resetButton.addEventListener('click', resetCanvas); runButton.addEventListener('click', processImage); } // 处理文件上传 function handleFileSelect(e) { const file = e.target.files[0]; if (!file || !file.type.startsWith('image/')) return; const reader = new FileReader(); reader.onload = function(event) { const img = new Image(); img.onload = function() { state.image = img; state.imgWidth = img.width; state.imgHeight = img.height; // 初始化画布尺寸 canvas.width = img.width; canvas.height = img.height; overlay.width = img.width; overlay.height = img.height; // 创建离屏 Mask 遮罩画布 state.maskCanvas = document.createElement('canvas'); state.maskCanvas.width = img.width; state.maskCanvas.height = img.height; state.maskCtx = state.maskCanvas.getContext('2d', { willReadFrequently: true }); // 绘制原图到主画布 ctx.drawImage(img, 0, 0); // 清空遮罩与预览 clearMask(); resultImg.removeAttribute('src'); resultImg.classList.add('hidden'); placeholderTip.classList.remove('hidden'); downloadBtn.classList.add('hidden'); runButton.disabled = false; updateStatus('图片加载成功!点击对象可智能选中,或用画笔涂抹。'); }; img.src = event.target.result; }; reader.readAsDataURL(file); } // 清空遮罩 function clearMask() { if (!state.maskCtx) return; state.maskCtx.clearRect(0, 0, state.imgWidth, state.imgHeight); redrawOverlay(); } // 重置操作 function resetCanvas() { if (!state.image) return; ctx.clearRect(0, 0, state.imgWidth, state.imgHeight); ctx.drawImage(state.image, 0, 0); clearMask(); resultImg.removeAttribute('src'); resultImg.classList.add('hidden'); placeholderTip.classList.remove('hidden'); downloadBtn.classList.add('hidden'); updateStatus('已重置画面'); } // 获取画布上的真实图片坐标 function getCanvasCoordinates(e) { const rect = overlay.getBoundingClientRect(); const scaleX = state.imgWidth / rect.width; const scaleY = state.imgHeight / rect.height; return { x: Math.floor((e.clientX - rect.left) * scaleX), y: Math.floor((e.clientY - rect.top) * scaleY) }; } // 指针按下事件 function handlePointerDown(e) { if (!state.image) return; const coords = getCanvasCoordinates(e); if (state.tool === 'select') { updateStatus('正在智能识别边缘并选中...'); smartEdgeSelect(coords.x, coords.y); } else { state.isDrawing = true; drawBrush(coords.x, coords.y); } } // 指针移动事件 function handlePointerMove(e) { if (!state.isDrawing || state.tool === 'select' || !state.image) return; const coords = getCanvasCoordinates(e); drawBrush(coords.x, coords.y); } // 指针抬起事件 function handlePointerUp() { state.isDrawing = false; } // 执行画笔涂抹 function drawBrush(x, y) { const mCtx = state.maskCtx; mCtx.save(); mCtx.beginPath(); mCtx.arc(x, y, state.brushSize, 0, Math.PI * 2); if (state.tool === 'add') { mCtx.fillStyle = 'rgba(255, 255, 255, 1)'; mCtx.fill(); } else if (state.tool === 'erase') { mCtx.globalCompositeOperation = 'destination-out'; mCtx.fill(); } mCtx.restore(); redrawOverlay(); } // 智能边界识别点选算法 function smartEdgeSelect(startX, startY) { const imgData = ctx.getImageData(0, 0, state.imgWidth, state.imgHeight); const data = imgData.data; const width = state.imgWidth; const height = state.imgHeight; const startIndex = (startY * width + startX) * 4; const targetR = data[startIndex]; const targetG = data[startIndex + 1]; const targetB = data[startIndex + 2]; const mCtx = state.maskCtx; mCtx.save(); mCtx.beginPath(); const tolerance = 35; const visited = new Uint8Array(width * height); const queue = [[startX, startY]]; let count = 0; while (queue.length > 0 && count < 20000) { const [x, y] = queue.pop(); if (x < 0 || x >= width || y < 0 || y >= height) continue; const idx = y * width + x; if (visited[idx]) continue; visited[idx] = 1; const pIdx = idx * 4; const r = data[pIdx]; const g = data[pIdx + 1]; const b = data[pIdx + 2]; const diff = Math.abs(r - targetR) + Math.abs(g - targetG) + Math.abs(b - targetB); if (diff < tolerance * 3) { mCtx.rect(x, y, 1, 1); count++; queue.push([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]); } } mCtx.fillStyle = 'rgba(255, 255, 255, 1)'; mCtx.fill(); mCtx.restore(); redrawOverlay(); updateStatus('智能边缘识别完成!可用画笔继续微调。'); } // 重新绘制半透明遮罩 function redrawOverlay() { overlayCtx.clearRect(0, 0, state.imgWidth, state.imgHeight); if (!state.maskCanvas) return; overlayCtx.save(); overlayCtx.drawImage(state.maskCanvas, 0, 0); overlayCtx.globalCompositeOperation = 'source-in'; overlayCtx.fillStyle = 'rgba(37, 99, 235, 0.45)'; overlayCtx.fillRect(0, 0, state.imgWidth, state.imgHeight); overlayCtx.restore(); } /** * 核心新增:真实后端 POST 请求函数 * 将原图与涂抹 Mask 打包发送至服务器端 AI 模型接口 */ async function processImage() { if (!state.image) return; loadingMask.classList.remove('hidden'); updateStatus('正在打包图像与选区,向后端 AI 模型发起 POST 请求...'); try { // 1. 将画布和遮罩转为 Blob 对象 const originBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.95)); const maskBlob = await new Promise(resolve => state.maskCanvas.toBlob(resolve, 'image/png')); // 2. 组装 FormData const formData = new FormData(); formData.append('image', originBlob, 'input.jpg'); formData.append('mask', maskBlob, 'mask.png'); formData.append('mode', state.mode); // 'cutout' 或 'inpaint' // 3. 发送 POST 请求到后端接口(例如你的 Python FastAPI / Flask 后端路由 '/process') const response = await fetch('/process', { method: 'POST', body: formData }); if (!response.ok) { throw new Error(`后端服务器响应错误: ${response.status} ${response.statusText}`); } // 4. 获取后端返回的处理后图片流 const resultBlob = await response.blob(); const resultUrl = URL.createObjectURL(resultBlob); // 5. 渲染结果 resultImg.src = resultUrl; resultImg.classList.remove('hidden'); placeholderTip.classList.add('hidden'); downloadBtn.href = resultUrl; downloadBtn.download = `result_${state.mode}.png`; downloadBtn.classList.remove('hidden'); updateStatus('AI 处理成功!'); } catch (error) { console.error('POST 请求失败:', error); updateStatus(`连接后端失败: ${error.message}(请检查后端服务是否启动)`); // 降级兜底:若后端未启动,自动模拟本地回显方便你调试界面 setTimeout(() => { const fallbackUrl = canvas.toDataURL('image/jpeg'); resultImg.src = fallbackUrl; resultImg.classList.remove('hidden'); placeholderTip.classList.add('hidden'); downloadBtn.href = fallbackUrl; downloadBtn.classList.remove('hidden'); updateStatus('(提示:当前为前端降级预览,请确认后端的 /process 接口已就绪)'); }, 1000); } finally { loadingMask.classList.add('hidden'); } } // 更新状态提示 function updateStatus(text) { statusDiv.textContent = text; } // 启动初始化 init();