You need to enable JavaScript to run this app.
导航
Inpainting&OutPainting最佳实践
最近更新时间:2024.08.08 18:05:16首次发布时间:2024.07.31 10:55:34

效果图

原图

涂抹

获取mask

涂抹消除

涂抹编辑

智能扩图(比例)

智能扩图(画布)

图片

图片

图片

图片

图片

图片

图片

Inpainting

涂抹/识别需要操作的对象

前端涂抹Demo

  • 需要修改image.src = 'origin_painting.png';
  • 需要设置canvas的尺寸与图片的尺寸一致;// 设置canvas的尺寸
  • 涂抹完成后,右键保存到本地或上传到云端生成http链接
<!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>

抠出黑白mask

Go 获取mask Demo

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"
}

OutPainting

可选使用方式

类型

描述

交互逻辑建议

top、bottom 、left 、right

mask

示例图-原图

示例图-效果图

等比扩展

以图片中心将图片等比扩展,可以设置扩展倍数,考虑效果和性能,目前最大为1

前端做数个固定比例扩展的按钮如扩展10%、20%,让用户点选扩展比例

参数同时传相同数值,数值越大延迟越高

不传

图片
原图

图片
扩展1倍

画幅扩展

以原图为中心,将图片扩展为特定画幅比例的图片。比如1张1:1的方图,扩展生成16:9的横图。一般默认原图大小不变,仅扩展生成目标画幅空白区域。

限制/检测客户输入图图片比例(如要求客户输入图片必须为1:1的图片),固定数个扩展的比例(如16:9)
服务端预置换算逻辑,预置输入图与输出图尺寸转换对应参数

根据换算逻辑传相应参数

不传

图片
1:1原图

图片
16:9画幅

四边扩展

以原图为中心,支持自定义扩展逻辑,比如向上扩展20%,向右扩展60%,向左扩展40%。逻辑也是默认原图大小不变,仅扩展生成自由设置的区域。

支持用户自定义输入,或者前端/后段固定指定尺寸

在这里传相应参数

不传

图片
原图

图片
向上扩展20%,向右扩展60%,向左扩展40%

画布扩展

可以定义一个画布(可以固定尺寸,也可以支持自定义调节),支持原图在画布里的位置、大小、旋转等交互,最终生成固定画布的扩展图;

前端有画布,支持用户上传图片后在画布内调整

不传

传画布相应信息

图片

传top、left方式

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"
}

使用mask方式

获取延边后的图

前端扩展Demo

  • 修改图片路径:img.src = '女侧119K.JPG';
  • 修改画布尺寸 :canvas id="canvas" width="1500" height="1500
  • 延边完成后,右键保存到本地或上传到云端生成http链接
<!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>

获取黑白mask

Go 获取mask demo
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
}

使用延边图及其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"
}