本文为个人学习笔记整理,仅供交流参考,非专业教学资料,内容请自行甄别。
通过学习获取的图片仅可用于个人技术研究(如测试下载逻辑、解析代码),不得用于商业用途(如制作产品素材、二次分发),也不能传播涉及版权或隐私的内容。


前言

  本篇主要介绍在图库类型的项目中,如何运用AI对图片进行一系列的优化操作。本篇使用到的是阿里云百炼。


一、AI扩图

1.1、概述

  阿里云百炼的官方文档:阿里云百炼
  调用大模型,首先需要获取API Key,这一块跟着文档的步骤即可实现:
在这里插入图片描述
  在本项目中,用到的是图像画面扩展的功能:
在这里插入图片描述
  查看官方文档,该API采用的是异步调用的模式,即分为创建提交任务查询任务结果两部分:

  1. 创建任务获取任务ID,接口返回任务ID,可根据任务ID查询图像生成的结果。该 task_id的查询有效期为 24 小时。
  2. 使用步骤1中获取的 task_id,通过 GET 请求轮询任务查询接口,直到task_status 变为 SUCCEEDED。任务成功后,响应中会包含生成的图像 URL。

  创建任务,这一步一定是要在后端实现的,因为调用接口涉及到API key,这样的敏感信息是不能存在前端的。而轮询获取任务结果的操作,则是放在了前端,通过定时器,每隔一段时间向后端发送请求,获取最新的结果。

1.2、项目实现

  首先是要在配置文件中对API key进行设置:
在这里插入图片描述
  然后编写创建任务和获取任务结果的代码,这里采用直接发送Http请求的方式,对应的请求参数和示例如图:
在这里插入图片描述
在这里插入图片描述

@Slf4j
@Component
public class AliyunAIService {

    @Value("${aliYunAi.apiKey}")
    private String apiKey;

    /**
     * 创建任务获取任务ID请求地址
     */
    private final String createTaskHttpPostUrl = "https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting";

    /**
     * 查询任务结果请求地址
     */
    private final String queryTaskResHttpGetUrl = "https://dashscope.aliyuncs.com/api/v1/tasks/";


    /**
     * 创建任务获取任务ID
     *
     * @param request
     * @return
     */
    public CreateOutPaintingTaskResponse createTaskHttpPost(CreateOutPaintingTaskRequest request) {
        ThrowUtils.throwIf(ObjUtil.isEmpty(request), ErrorCode.PARAMS_ERROR, "请求参数为空");
        // 发送POST请求
        HttpRequest httpRequest = HttpRequest.post(createTaskHttpPostUrl)
                .header("X-DashScope-Async", "enable")
                .header("Authorization", "Bearer " + apiKey)
                .header("Content-Type", "application/json")
                .body(JSONUtil.toJsonStr(request));

        //解析结果
        try (HttpResponse httpResponse = httpRequest.execute()) {
            ThrowUtils.throwIf(!httpResponse.isOk(), ErrorCode.OPERATION_ERROR, "创建任务失败!");
            CreateOutPaintingTaskResponse response = JSONUtil.toBean(httpResponse.body(), CreateOutPaintingTaskResponse.class);
            ThrowUtils.throwIf(ObjUtil.isEmpty(response), ErrorCode.OPERATION_ERROR, "响应结果为空!");
            ThrowUtils.throwIf(!response.getOutput().getTaskStatus().equals("PENDING"), ErrorCode.OPERATION_ERROR, "响应状态异常!");
            return response;
        }
    }

    /**
     * 根据任务ID查询任务结果
     *  这里不能因为返回的不是success就返回前端失败,因为前端需要轮询,可能任务在处理中
     * @param taskId
     * @return
     */
    public GetOutPaintingTaskResponse createQueryTaskResHttpGet(String taskId) {
        log.info("任务ID:{},开始执行",taskId);
        ThrowUtils.throwIf(StrUtil.isBlank(taskId), ErrorCode.PARAMS_ERROR, "请求参数为空");
        // 发送Get请求
        HttpRequest httpRequest = HttpRequest.get(queryTaskResHttpGetUrl + taskId).header("Authorization", "Bearer " + apiKey);
        //解析结果
        try (HttpResponse httpResponse = httpRequest.execute()) {
            GetOutPaintingTaskResponse response = JSONUtil.toBean(httpResponse.body(), GetOutPaintingTaskResponse.class);
            ThrowUtils.throwIf(ObjUtil.isEmpty(response), ErrorCode.OPERATION_ERROR, "响应结果为空!");
            log.info("任务ID:{},执行完成",taskId);
            return response;
        }
    }
}

  扩图任务请求封装类,在xScale和yScale属性上,加入了jackson的@JsonProperty注解,原因是Spring MVC对于属性名第二个字母大写的情况,无法进行映射。注意,这里第二个字母大写,只是为了和接口响应的值相对应,将接口返回的x_scale,y_scale转换为了规范的驼峰命名。在自定义实体类的情况下,应该避免这样的命名方式,如email,不要定义成eMail。

/**
 * 扩图任务请求类
 */
@Data
public class CreateOutPaintingTaskRequest implements Serializable {

    /**
     * 模型,例如 "image-out-painting"
     */
    private String model = "image-out-painting";

    /**
     * 输入图像信息
     */
    private Input input;

    /**
     * 图像处理参数
     */
    private Parameters parameters;

    @Data
    public static class Input {
        /**
         * 必选,图像 URL
         */
        @Alias("image_url")
        private String imageUrl;
    }

    @Data
    public static class Parameters implements Serializable {
        /**
         * 可选,逆时针旋转角度,默认值 0,取值范围 [0, 359]
         */
        private Integer angle;

        /**
         * 可选,输出图像的宽高比,默认空字符串,不设置宽高比
         * 可选值:["", "1:1", "3:4", "4:3", "9:16", "16:9"]
         */
        @Alias("output_ratio")
        private String outputRatio;

        /**
         * 可选,图像居中,在水平方向上按比例扩展,默认值 1.0,范围 [1.0, 3.0]
         */
        @Alias("x_scale")
        @JsonProperty("xScale")
        private Float xScale;

        /**
         * 可选,图像居中,在垂直方向上按比例扩展,默认值 1.0,范围 [1.0, 3.0]
         */
        @Alias("y_scale")
        @JsonProperty("yScale")
        private Float yScale;

        /**
         * 可选,在图像上方添加像素,默认值 0
         */
        @Alias("top_offset")
        private Integer topOffset;

        /**
         * 可选,在图像下方添加像素,默认值 0
         */
        @Alias("bottom_offset")
        private Integer bottomOffset;

        /**
         * 可选,在图像左侧添加像素,默认值 0
         */
        @Alias("left_offset")
        private Integer leftOffset;

        /**
         * 可选,在图像右侧添加像素,默认值 0
         */
        @Alias("right_offset")
        private Integer rightOffset;

        /**
         * 可选,开启图像最佳质量模式,默认值 false
         * 若为 true,耗时会成倍增加
         */
        @Alias("best_quality")
        private Boolean bestQuality;

        /**
         * 可选,限制模型生成的图像文件大小,默认值 true
         * - 单边长度 <= 10000:输出图像文件大小限制为 5MB 以下
         * - 单边长度 > 10000:输出图像文件大小限制为 10MB 以下
         */
        @Alias("limit_image_size")
        private Boolean limitImageSize;

        /**
         * 可选,添加 "Generated by AI" 水印,默认值 true
         */
        @Alias("add_watermark")
        private Boolean addWatermark = false;
    }
}

  扩图任务响应封装类:

/**
 * 扩图任务响应类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateOutPaintingTaskResponse {

    private Output output;

    /**
     * 表示任务的输出信息
     */
    @Data
    public static class Output {

        /**
         * 任务 ID
         */
        private String taskId;

        /**
         * 任务状态
         * <ul>
         *     <li>PENDING:排队中</li>
         *     <li>RUNNING:处理中</li>
         *     <li>SUSPENDED:挂起</li>
         *     <li>SUCCEEDED:执行成功</li>
         *     <li>FAILED:执行失败</li>
         *     <li>UNKNOWN:任务不存在或状态未知</li>
         * </ul>
         */
        private String taskStatus;
    }

    /**
     * 接口错误码。
     * <p>接口成功请求不会返回该参数。</p>
     */
    private String code;

    /**
     * 接口错误信息。
     * <p>接口成功请求不会返回该参数。</p>
     */
    private String message;

    /**
     * 请求唯一标识。
     * <p>可用于请求明细溯源和问题排查。</p>
     */
    private String requestId;

}

  查询任务响应封装类:

/**
 * 查询任务响应类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GetOutPaintingTaskResponse {

    /**
     * 请求唯一标识
     */
    private String requestId;

    /**
     * 输出信息
     */
    private Output output;

    /**
     * 表示任务的输出信息
     */
    @Data
    public static class Output {

        /**
         * 任务 ID
         */
        private String taskId;

        /**
         * 任务状态
         * <ul>
         *     <li>PENDING:排队中</li>
         *     <li>RUNNING:处理中</li>
         *     <li>SUSPENDED:挂起</li>
         *     <li>SUCCEEDED:执行成功</li>
         *     <li>FAILED:执行失败</li>
         *     <li>UNKNOWN:任务不存在或状态未知</li>
         * </ul>
         */
        private String taskStatus;

        /**
         * 提交时间
         * 格式:YYYY-MM-DD HH:mm:ss.SSS
         */
        private String submitTime;

        /**
         * 调度时间
         * 格式:YYYY-MM-DD HH:mm:ss.SSS
         */
        private String scheduledTime;

        /**
         * 结束时间
         * 格式:YYYY-MM-DD HH:mm:ss.SSS
         */
        private String endTime;

        /**
         * 输出图像的 URL
         */
        private String outputImageUrl;

        /**
         * 接口错误码
         * <p>接口成功请求不会返回该参数</p>
         */
        private String code;

        /**
         * 接口错误信息
         * <p>接口成功请求不会返回该参数</p>
         */
        private String message;

        /**
         * 任务指标信息
         */
        private TaskMetrics taskMetrics;
    }

    /**
     * 表示任务的统计信息
     */
    @Data
    public static class TaskMetrics {

        /**
         * 总任务数
         */
        private Integer total;

        /**
         * 成功任务数
         */
        private Integer succeeded;

        /**
         * 失败任务数
         */
        private Integer failed;
    }
}

  前端部分,则是要定义一个定时器,并且需要记录后端传递的taskId。要注意定时器需要关闭的场景,后端明确返回成功或失败,接口失败,接口报错,关闭页面时,都需要清理计时器,并且置空taskId。

//轮询定时器
let pollingTimer: NodeJS.Timeout = null
const loading = ref<boolean>(false)

/**
 * 开启轮询
 */
const startPolling = () => {
  if (!taskId.value) {
    return
  }
  pollingTimer = setInterval(async () => {
    try {
      //调用后端查询AI处理图片任务结果的接口
      const resp = await queryPicHandletaskUsingGet({
        taskId: taskId.value,
      })
      //接口成功
      if (resp.data.code === 0) {
        //成功
        if (resp.data.data?.output?.taskStatus === 'SUCCEEDED') {
          aiHandleResUrl.value = resp.data.data?.output?.outputImageUrl
          stopPolling()
        }
        //失败
        if (resp.data.data?.output?.taskStatus === 'FAILED') {
          stopPolling()
        }
      } //接口失败
      else {
        stopPolling()
      }
    } catch (error) {
      console.log('调用后端查询AI处理图片任务结果的接口错误', error)
      stopPolling()
    }
  }, 3000)
}

/**
 * 结束轮询
 */
function stopPolling() {
  if (pollingTimer) {
    clearInterval(pollingTimer)
    loading.value = false
    pollingTimer = null
    taskId.value = null
  }
}

/**-
 * 生成图片 调用后端创建AI处理图片任务接口
 */
async function doAIHandler() {
  loading.value = true
  const resp = await createPicHandletaskUsingPost({
    pictureId: props.picture?.id,
    parameters: {
      xScale: 2,
      yScale: 2,
    },
  })
  if (resp.data.code === 0) {
    message.success('后台处理中,请勿关闭窗口,稍后查证')
    //记录任务ID
    taskId.value = resp.data.data?.output?.taskId
    //开启任务轮询
    startPolling()
  } else {
    message.error(resp.data.message)
  }
}


onUnmounted(() => {
  stopPolling()
})

  最终效果,AI扩图有一定的限制,也应在前端给予友好的提示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐