原图 | 涂抹 | 获取mask | 涂抹消除 | 涂抹编辑 | 智能扩图(比例) | 智能扩图(画布) |
---|---|---|---|---|---|---|
<!DOCTYPE html> <html> <head> <title>Canvas Smear Mask Example</title> <style> body { position: relative; margin: 0; padding: 0; } #controls { position: absolute; left: 10px; top: 10px; z-index: 10; } canvas { border: 1px solid black; cursor: crosshair; display: block; /* 避免出现滚动条 */ margin: 0 auto; /* 水平居中画布 */ position: relative; /* 相对定位使其能够正确计算offsetLeft和offsetTop值 */ top: 20px; /* 按钮高度+间距 */ } </style> <script> window.onload = function () { var canvas = document.getElementById('smear-canvas'); var ctx = canvas.getContext('2d'); var mouseDown = false; // 设置canvas的尺寸,与待涂抹的原图尺寸一致 canvas.width = 1000; canvas.height = 1000; // 加载一个图片作为背景 var image = new Image(); image.onload = function () { ctx.drawImage(image, 0, 0, canvas.width, canvas.height); } image.src = '女侧119K.JPG'; // 你的图片路径 // 当鼠标在canvas上按下时开始涂抹 canvas.addEventListener('mousedown', function (e) { mouseDown = true; ctx.globalCompositeOperation = 'destination-out'; // 设置涂抹后显示下面的内容 }); // 当鼠标在canvas上移动时,如果是按下状态,则涂抹 canvas.addEventListener('mousemove', function (e) { if (mouseDown) { var x = e.pageX - canvas.offsetLeft; var y = e.pageY - canvas.offsetTop; ctx.beginPath(); ctx.arc(x, y, 10, 0, Math.PI * 2, false); // 绘制涂抹的圆形 ctx.fill(); } }); // 当鼠标释放或离开canvas时,停止涂抹 canvas.addEventListener('mouseup', function () { mouseDown = false; }); canvas.addEventListener('mouseleave', function () { mouseDown = false; }); // 添加保存画布内容到本地的功能 var saveButton = document.getElementById('saveButton'); saveButton.addEventListener('click', function () { var dataURL = canvas.toDataURL('image/png'); var link = document.createElement('a'); link.download = '标注结果.png'; link.href = dataURL; link.click(); }); }; </script> </head> <body> <div id="controls"> <button id="saveButton">保存到本地</button> </div> <canvas id="smear-canvas"></canvas> </body> </html>
package main import ( "image" "image/color" // 确保导入了支持的图像格式包 _ "image/jpeg" "image/png" "os" "github.com/cloudwego/hertz/pkg/common/hlog" ) // FileUrl2Image 从指定链接获取图片并转成Image func FileUrl2Image(fileUrl string) image.Image { // 发送get请求,拉取文件 resp, err := http.Get(fileUrl) if err != nil { hlog.Errorf("+++++HTTP GET error: %w", err) return nil } defer resp.Body.Close() // 检查HTTP响应状态码 if resp.StatusCode != http.StatusOK { hlog.Errorf("+++++HTTP Status not OK: %d", resp.StatusCode) return nil } img, _, err := image.Decode(resp.Body) if err != nil { hlog.Error("+++++解码图片失败, img:", img) hlog.Error("+++++解码图片失败, err:", err) } return img } // CreateMaskBasedOnAlpha 将使用传入图片的alpha通道值来创建一个遮罩 func createMaskBasedOnAlpha(img image.Image) image.Image { // 获取原始图片的边界 rect := img.Bounds() // 创建一个相同大小的mask图像(默认遮罩是全黑的,也就是透明的) mask := image.NewAlpha(rect) // 遍历原始图片的每个像素 for y := rect.Min.Y; y < rect.Max.Y; y++ { for x := rect.Min.X; x < rect.Max.X; x++ { // 取出原始图片的像素 _, _, _, a := img.At(x, y).RGBA() // RGBA方法返回的颜色值范围是0-65535 // 将其转换为0-255范围,以便与通常的alpha值比较。 alpha := uint8(a >> 8) // 基于alpha值判断是否应该在遮罩中保留这个像素 if alpha > 128 { // 假设alpha大于128我们才保留这个像素 mask.SetAlpha(x, y, color.Alpha{A: 0}) // 不透明 } else { mask.SetAlpha(x, y, color.Alpha{A: 255}) // 完全透明 } } } return mask } // saveMaskToFile 将遮罩图像保存到文件中 // mask: 遮罩信息 // maskSavePath: mask转成图片的路径 func saveMaskToFile(mask image.Image, maskSavePath string) error { // 创建一个文件用于保存遮罩 ofi, err := os.OpenFile(maskSavePath, os.O_RDWR|os.O_CREATE, os.ModePerm) if err != nil { return err } defer ofi.Close() // 使用png格式将遮罩编码并写入文件 err = png.Encode(ofi, mask) if err != nil { return err } return nil } func GenerateMask(img image.Image, maskPath string) { // 从文件中解码图片 err := saveMaskToFile(createMaskBasedOnAlpha(img), maskPath) if err != nil { hlog.Error("+++++保存mask失败,err:", err) } } func main() { imgUrl := "https://xxxx" // 更换成第一步涂抹完成的图片链接 GenerateMask(FileUrl2Image(imgUrl), "test-mask.png") // 此时生成的test-mask.png即为接口调用使用的mask }
在完成第1,2步后,开始调用 inpainting涂抹消除接口
// 请求参数Demo { "binary_data_base64": ["原图", "原图标注后的mask"], "req_key": "i2i_inpainting", "scale": 7, "seed": 0, "steps": 30, "strength": 0.8 }
// 返回参数Demo { "code": 10000, "data": { "algorithm_base_resp": { "status_code": 0, "status_message": "Success" }, "binary_data_base64": ["消除图base64"], "request_id": "740d84695b1a51a5e36f7559a29dbe563f003b99add6c17f419badda749ccd21" }, "message": "Success", "request_id": "2024031411153853949433144E29001798", "status": 10000, "time_elapsed": "3.940519855s" }
在完成第1,2步后,开始调用 inpainting涂抹编辑接口
// 请求参数Demo { "binary_data_base64": ["原图", "原图标注后的mask"], "custom_prompt": "一只小狗", // 写入Prompt,AIGC生成取代的内容 "req_key": "i2i_inpainting_edit", "scale": 5, "seed": -1, "steps": 25 }
// 返回参数Demo { "code": 10000, "data": { "algorithm_base_resp": { "status_code": 0, "status_message": "Success" }, "binary_data_base64": ["编辑后的图base64"], "request_id": "d1741fd2176ae73ea6e17ae911bc81a5d6848449357c7024fe3be30701412726" }, "message": "Success", "request_id": "2024031414442379E5503DD25AA800AB0F", "status": 10000, "time_elapsed": "2.173378731s" }
类型 | 描述 | 交互逻辑建议 | top、bottom 、left 、right | mask | 示例图-原图 | 示例图-效果图 |
---|---|---|---|---|---|---|
等比扩展 | 以图片中心将图片等比扩展,可以设置扩展倍数,考虑效果和性能,目前最大为1 | 前端做数个固定比例扩展的按钮如扩展10%、20%,让用户点选扩展比例 | 参数同时传相同数值,数值越大延迟越高 | 不传 |
|
|
画幅扩展 | 以原图为中心,将图片扩展为特定画幅比例的图片。比如1张1:1的方图,扩展生成16:9的横图。一般默认原图大小不变,仅扩展生成目标画幅空白区域。 | 限制/检测客户输入图图片比例(如要求客户输入图片必须为1:1的图片),固定数个扩展的比例(如16:9) | 根据换算逻辑传相应参数 | 不传 |
|
|
四边扩展 | 以原图为中心,支持自定义扩展逻辑,比如向上扩展20%,向右扩展60%,向左扩展40%。逻辑也是默认原图大小不变,仅扩展生成自由设置的区域。 | 支持用户自定义输入,或者前端/后段固定指定尺寸 | 在这里传相应参数 | 不传 |
|
|
画布扩展 | 可以定义一个画布(可以固定尺寸,也可以支持自定义调节),支持原图在画布里的位置、大小、旋转等交互,最终生成固定画布的扩展图; | 前端有画布,支持用户上传图片后在画布内调整 | 不传 | 传画布相应信息 |
按outpainting智能扩图接口文档传参,计算扩展比例即可
// 请求参数demo { "req_key": "i2i_outpainting", "prompt": "蓝色的海洋", "binary_data_base64": ["原图base64"], "scale": 7, "seed": -1, "steps": 30, "strength": 0.8, "top": 0.1 "bottom": 0.1, "left": 1, "right": 1, "max_height": 1920, "max_width": 1920 }
// 返回参数demo { "code": 10000, "data": { "algorithm_base_resp": { "status_code": 0, "status_message": "Success" }, "binary_data_base64": ["扩展图base64"], "request_id": "977202e694283d9e430e6d91d3e0bc6540077ea26de523ba7dfd3856409db693" }, "message": "Success", "request_id": "20240314153408C09A6A1651795D028F5A", "status": 10000, "time_elapsed": "3.9405467s" }
前端扩展Demo
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Interactive Image Manipulation on Canvas</title> <style> .container { text-align: center; } .sidebar { text-align: left; float: left; margin-right: 20px; font-family: Arial, sans-serif; font-size: 14px; width: 150px; /* Adjust width as per requirement */ } #canvasContainer { position: relative; display: inline-block; margin-bottom: 10px; } #canvas { border: 1px solid #000; } .instructions { margin-bottom: 10px; line-height: 1.5; } .instructions strong { color: #333; } </style> </head> <body> <div class="container"> <div id="canvasContainer"> <div class="sidebar"> <div class="instructions"> <strong>操作说明:</strong><br> - 拖动: 鼠标左键点击图片并拖动<br> - 缩放: 在图片上按住 Alt 键并上下拖动鼠标<br> - 旋转: 在图片上按住 Shift 键并左右拖动鼠标<br> </div> <button id="saveBtn">Save Image</button> </div> <canvas id="canvas" width="1500" height="1500"></canvas> </div> </div> <script> var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); var img = new Image(); var imgLoaded = false; var dragging = false, rotating = false, scaling = false; var imgX = canvas.width / 2; var imgY = canvas.height / 2; var imgScale = 1.0; var imgRotate = 0; var prevX, prevY; var ROTATION_SENSITIVITY = 0.5; // You can adjust the sensitivity var SCALE_SENSITIVITY = 0.001; // You can adjust the sensitivity // 图片路径,替换为实际图片路径 img.src = '女侧119K.JPG'; img.onload = function () { imgLoaded = true; drawImage(); } function drawImage() { if (!imgLoaded) return; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.save(); ctx.translate(imgX, imgY); ctx.rotate(imgRotate * Math.PI / 180); ctx.scale(imgScale, imgScale); ctx.drawImage(img, -img.width / 2, -img.height / 2); ctx.restore(); } function getMousePos(evt) { var rect = canvas.getBoundingClientRect(); return { x: evt.clientX - rect.left, y: evt.clientY - rect.top }; } canvas.addEventListener('mousedown', function (evt) { var pos = getMousePos(evt); prevX = pos.x; prevY = pos.y; scaling = evt.altKey; rotating = evt.shiftKey; dragging = !(scaling || rotating); }); canvas.addEventListener('mousemove', function (evt) { if (!dragging && !rotating && !scaling) return; var pos = getMousePos(evt); var dx = pos.x - prevX; var dy = pos.y - prevY; prevX = pos.x; prevY = pos.y; if (dragging) { imgX += dx; imgY += dy; } else if (rotating) { imgRotate += dx * ROTATION_SENSITIVITY; } else if (scaling) { var scaleFactor = 1 + dy * SCALE_SENSITIVITY; imgScale *= scaleFactor; } drawImage(); }); canvas.addEventListener('mouseup', function () { dragging = false; rotating = false; scaling = false; }); canvas.addEventListener('mouseleave', function () { dragging = false; rotating = false; scaling = false; }); document.getElementById('saveBtn').addEventListener('click', function () { var dataURL = canvas.toDataURL('image/png'); var link = document.createElement('a'); link.download = '延边图.png'; link.href = dataURL; link.click(); }); </script> </body> </html>
package main import ( "image" "image/color" // 确保导入了支持的图像格式包 _ "image/jpeg" "image/png" "os" "github.com/cloudwego/hertz/pkg/common/hlog" ) // FileUrl2Image 从指定链接获取图片并转成Image func FileUrl2Image(fileUrl string) image.Image { // 发送get请求,拉取文件 resp, err := http.Get(fileUrl) if err != nil { hlog.Errorf("+++++HTTP GET error: %w", err) return nil } defer resp.Body.Close() // 检查HTTP响应状态码 if resp.StatusCode != http.StatusOK { hlog.Errorf("+++++HTTP Status not OK: %d", resp.StatusCode) return nil } img, _, err := image.Decode(resp.Body) if err != nil { hlog.Error("+++++解码图片失败, img:", img) hlog.Error("+++++解码图片失败, err:", err) } return img } // CreateMaskBasedOnAlpha 将使用传入图片的alpha通道值来创建一个遮罩 func createMaskBasedOnAlpha(img image.Image) image.Image { // 获取原始图片的边界 rect := img.Bounds() // 创建一个相同大小的mask图像(默认遮罩是全黑的,也就是透明的) mask := image.NewAlpha(rect) // 遍历原始图片的每个像素 for y := rect.Min.Y; y < rect.Max.Y; y++ { for x := rect.Min.X; x < rect.Max.X; x++ { // 取出原始图片的像素 _, _, _, a := img.At(x, y).RGBA() // RGBA方法返回的颜色值范围是0-65535 // 将其转换为0-255范围,以便与通常的alpha值比较。 alpha := uint8(a >> 8) // 基于alpha值判断是否应该在遮罩中保留这个像素 if alpha > 128 { // 假设alpha大于128我们才保留这个像素 mask.SetAlpha(x, y, color.Alpha{A: 0}) // 不透明 } else { mask.SetAlpha(x, y, color.Alpha{A: 255}) // 完全透明 } } } return mask } // saveMaskToFile 将遮罩图像保存到文件中 // mask: 遮罩信息 // maskSavePath: mask转成图片的路径 func saveMaskToFile(mask image.Image, maskSavePath string) error { // 创建一个文件用于保存遮罩 ofi, err := os.OpenFile(maskSavePath, os.O_RDWR|os.O_CREATE, os.ModePerm) if err != nil { return err } defer ofi.Close() // 使用png格式将遮罩编码并写入文件 err = png.Encode(ofi, mask) if err != nil { return err } return nil } func GenerateMask(img image.Image, maskPath string) { // 从文件中解码图片 err := saveMaskToFile(createMaskBasedOnAlpha(img), maskPath) if err != nil { hlog.Error("+++++保存mask失败,err:", err) } } func main() { imgUrl := "https://xxxx" // 更换成第一步涂抹完成的图片链接 GenerateMask(FileUrl2Image(imgUrl), "test-mask.png") // 此时生成的test-mask.png即为接口调用使用的mask }
完成3.1、3.2步骤后,开始调用
智能扩图outpainting
接口
https://www.volcengine.com/docs/6791/1223722
// 请求参数Demo { "req_key": "i2i_outpainting", "prompt": "蓝色的海洋", "binary_data_base64": ["延边图base64", "延边图mask"], "scale": 7, "seed": -1, "steps": 30, "strength": 0.8 "max_height": 1920, "max_width": 1920, }
// 返回参数Demo { "code": 10000, "data": { "algorithm_base_resp": { "status_code": 0, "status_message": "Success" }, "binary_data_base64": ["扩展图base64"], "request_id": "b412f34efa44b30c039654f3dfafd135795a8eb4a374cf8d9a1548bf01b27543" }, "message": "Success", "request_id": "20240314160151EF84082D2A4970000712", "status": 10000, "time_elapsed": "4.495273592s" }