AI 擴圖功能
需求分析
隨著 AI 的高速發展,AI 幾乎可以應用到任何傳統業務中,增強應用的功能,帶給用戶更好的體驗。
對于圖庫網站來說,AI 也有非常多的應用空間,比如可以利用 AI 繪圖大模型來編輯圖片,實現擴圖、擦除補全、圖配文、去水印等功能。
以 AI 擴圖功能為例,用戶可以選擇一張已上傳的圖片,通過 AI 編輯得到新的圖片,并根據情況自行選擇是否保存。
該功能不限制僅在空間內使用,公共圖庫也可以支持。
方案設計
1. AI 繪圖大模型選擇
AI繪圖大模型我們自己是搞不來的,可以選擇一個市面上支持AI繪圖的大模型。
選擇 AI 大模型時,我們最關注的應該是生成效果、生成速度還有價格了吧?當然,對我們學習來說,最關注的還是價格,畢竟繪畫大模型的費用不低。
國內的 AI 繪圖大模型比較推薦阿里云百煉,它是一站式的大模型開發及應用構建平臺,可以通過簡單的界面操作,在5分鐘內開發出一款大模型應用,并在線體驗效果。
創建好應用后,利用官方提供的 API 或SDK,直接通過幾行代碼,就能在項目中使用大模型應用:
通過閱讀 官方文檔,發現它是支持AI圖像編輯與生成功能的,包括 AI 擴圖,支持 HTTP 調用,符合我們的需求。
在控制臺也能看到對應的圖像畫面擴展模型:
百煉的大模型提供了新人免費額度,可以通過文檔或者點進大模型了解,對于學習用來說足夠了:
經過測試,圖片生成效果、生成速度都是不錯的,因此,本項目將選用阿里云百煉實現AI擴圖功能。
建議:之前沒接觸過類似 AI 大模型平臺的同學,先多利用網頁控制臺熟悉 AI 大模型的 Prompt,了解不同大模型的區別。
推薦學習網站:WaytoAGI-通往AGI之路,最好的 AI 知識庫和工具站
2. 調用方式
通過閱讀 AI 圖像擴展的官方文檔,我們發現,API 只支持異步方式調用。
這是因為 AI 繪畫任務計算量大且耗時長,同步調用會導致服務器線程長時間被單個任務占用,限制了并發處理能力,增加了超時和系統崩潰的風險。
通過異步調用,服務器可以將任務放入隊列中,合理調度資源,避免阻塞主線程,從而更高效地服務多個用戶請求,提升整體系統的穩定性和可擴展性。
特點:客戶端可以直接獲取到結果,調用更方便。
異步調用流程如下,客戶端需要在提交任務后,不斷輪詢請求,來檢查任務是否執行完成。
由于 AI 接口已經選擇了異步調用,所以我們作為要調用 AI 接口的客戶端,要使用輪詢的方式來檢查任務狀態是否為“已完成”,如果完成了,才可以獲取到生成的圖片。
那么是前端輪詢還是后端輪詢呢?
前端輪詢
- 流程:前端調用后端提交任務后得到任務 ID,然后通過定時器輪詢請求查詢任務狀態接口,直到任務完成或失敗。
- 示例代碼:
// 提交任務
async function submitTask() {const response = await fetch('/api/createTask', { method: 'POST' });const { taskId } = await response.json();checkTaskStatus(taskId);
}// 調用submitTask();// 檢查任務狀態
async function checkTaskStatus(taskId) {const intervalId = setInterval(async () => {const response = await fetch(`/api/taskStatus?taskId=${taskId}`);const { status, result } = await response.json();if (status === 'success') {console.log('Task completed:', result);clearInterval(intervalId); // 停止輪詢} else if (status === 'failed') {console.error('Task failed');clearInterval(intervalId); // 停止輪詢}}, 2000); // 每隔 2 秒輪詢
}
后端輪詢
- 流程:后端通過循環或定時任務檢測任務狀態,接口保持阻塞,直到任務完成或失敗,直接返回結果給前端。
- 示例代碼:
@RestController
public class TaskController {@PostMapping("/createTask")public String createTask() {String taskId = taskService.submitTask();return taskId;}@GetMapping("/waitForTask")public ResponseEntity<String> waitForTask(@RequestParam String taskId) {while (true) {String status = taskService.checkTaskStatus(taskId);if ("success".equals(status)) {return ResponseEntity.ok("Task completed");} else if ("failed".equals(status)) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Task failed");}try {Thread.sleep(2000); // 等待 2 秒后重試} catch (InterruptedException e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error occurred");}}}
}
- 后端輪詢問題:后端輪詢容易因為任務阻塞導致資源耗盡。
- 推薦方案:通常推薦前端輪詢。除非有明確的需求要求時,才考慮后端輪詢,比如任務結果需實時返回且對網絡請求數敏感。(或者學習時不想寫前端的同學哈哈)
- 選擇:此處我們也選擇前端輪詢方案實現。
💡 小貼士:從這個方案設計中,我們也能感受到,如果你同時了解前端和后端,可以結合二者設計出更合理的方案,而不是把所有的“重擔”都交給前端或者后端一方。所以企業中開需求評審會或者討論方案時,前后端需要緊密協作。
后端開發
1. AI 擴圖 API
(1) 創建 API Key
首先開發業務依賴的基礎能力,也就是 AI 擴圖 API。
1. 需要先進入阿里云百煉控制臺開通服務:
2. 開通推理能力:
3. 開通之后,我們要在控制臺獲取API Key,可參考文檔:
開通之后,在控制臺獲取 API Key,可參考文檔。
能夠在控制臺查看到 API Key,注意,API Key 一定不要對外泄露!
接下來,我們需要根據下面的請求頭使用 Java 來構造擴圖請求實體類數據模型
:
通過閱讀文檔發現,百煉支持通過 SDK 或 HTTP 調用。
雖然官方寫的支持 Java SDK,但 AI 擴圖功能中對 SDK 的介紹非常少,此處考慮到兼容性,我們還是使用 HTTP 調用。
4. 由于使用異步的方式,需要開發創建任務和查詢結果 2 個 API。
5. 填寫配置文件:在配置文件中填寫獲取到的 apiKey
:
# 阿里云 AI 配置
aliYunAi:apiKey: xxxx
(2) 創建請求參數接收類
在 api
包下新建 aliyunai
包,存放阿里云 AI 相關代碼。
在 aliyunai.model
包下新建數據模型類,可以讓 AI 根據官方文檔中的請求響應信息,自動生成請求實體類,無需自己手動編寫。
復制下面的請求體內容,交給 AI 生成:
code is cheap, show me the talk !
由于每個 AI 圖片處理操作的請求響應都有一些區別,所以單獨給 AI 擴圖功能編寫具體的請求響應類。創建擴圖任務請求類:
@Data
public class CreateOutPaintingTaskRequest implements Serializable {/*** 模型,例如 "image-out-painting"*/private String model = "image-out-painting";/*** 輸入圖像信息*/private Input input;/*** 圖像處理參數*/private Parameters parameters;@Datapublic static class Input {/*** 必選,圖像 URL*/@Alias("image_url")private String imageUrl;}@Datapublic 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;}
}
注意:上述代碼中,某些字段打上了 Hutool 工具類的 @Alias
注解。這個注解僅對 Hutool 的 JSON 轉換生效
,對 SpringMVC 的 JSON 轉換沒有任何影響。
這里有一個很坑的地方,經過測試發現,前端如果傳遞參數名
xScale
,是無法賦值給xScale
字段的;但是傳遞參數名xscale
,就可以賦值。這是因為 SpringMVC 對于第二個字母是大寫的參數無法映射(和參數類別無關)。參考博客
解決方案:給這些字段增加 @JsonProperty
注解。
/*** 可選,圖像居中,在水平方向上按比例擴展,默認值 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;
為什么 SpringMVC 要這樣設計,通過查閱了解到,這是因為 Jackson 在處理字段名與 JSON 屬性名映射時,會依賴 Java 的
標準命名規范
和反射 API
。
- 舉個例子,根據 JavaBean 的規范,屬性名稱與其訪問器方法(getter 和 setter)之間的映射規則是:如果屬性名以小寫字母開頭,第二個字母是大寫(如
geteMail()
和seteMail()
。- 但 Jackson 會嘗試推斷屬性名為
記住結論即可:
SpringMVC 默認的序列化器 Jackson 在字段名的第二個字母為大寫時,無法正確映射;需要使用 @JsonProperty("yScale") 這樣的注解正確映射
(3) 創建擴圖任務響應類
這個類同理,不要自己寫,直接使用 AI 生成:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateOutPaintingTaskResponse {private Output output;/*** 表示任務的輸出信息*/@Datapublic 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;
}
(4) 查詢任務響應類
根據官方文檔響應的說明
,使用 AI 生成對應的查詢任務響應類
:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GetOutPaintingTaskResponse {/*** 請求唯一標識*/private String requestId;/*** 輸出信息*/private Output output;/*** 表示任務的輸出信息*/@Datapublic 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;}/*** 表示任務的統計信息*/@Datapublic static class TaskMetrics {/*** 總任務數*/private Integer total;/*** 成功任務數*/private Integer succeeded;/*** 失敗任務數*/private Integer failed;}
}
(5) 大模型調用API 開發
開發 API 調用類,通過 Hutool 的 HTTP 請求工具類來調用阿里云百煉的 API。
注解3:創建任務的請求地址,在官方文檔中可以找到
注解4:查詢任務狀態的請求地址,在官方文檔中同樣可以找到
注解6:根據官方文檔填寫請求頭
注解7:
注解 11:填寫查詢任務需要發送的請求頭
@Slf4j
@Component // 1. 這個類需要讀取配置文件中的 APIKey
public class AliYunAiApi {// 2. 使用 @Value 注解 (必須是 Spring 包), 讀取需要的配置文件@Value("${aliYunAi.apiKey}")private String apiKey;// 3. 創建任務地址public static final String CREATE_OUT_PAINTING_TASK_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting";// 4. 查詢任務狀態 %s 用于替換實際任務的 {task_id}public static final String GET_OUT_PAINTING_TASK_URL = "https://dashscope.aliyuncs.com/api/v1/tasks/%s";// 5. 創建任務public CreateOutPaintingTaskResponse createOutPaintingTask(CreateOutPaintingTaskRequest createOutPaintingTaskRequest){if(createOutPaintingTaskRequest == null){throw new BusinessException(ErrorCode.OPERATION_ERROR, "擴圖參數為空");}// curl --location --request POST 'https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting' \
// --header "Authorization: Bearer $DASHSCOPE_API_KEY" \
// --header 'X-DashScope-Async: enable' \
// --header 'Content-Type: application/json' \
// --data '{
// "model": "image-out-painting",
// "input": {
// "image_url": "http://xxx/image.jpg"
// },
// "parameters":{
// "angle": 45,
// "x_scale":1.5,
// "y_scale":1.5
// }
// }'// 6. 復制上述請求, 然后構造 HTTP 請求, 可以用 AI 生成HttpRequest httpRequest = HttpRequest.post(CREATE_OUT_PAINTING_TASK_URL) // 注解 3 的創建請求地址.header("Authorization", "Bearer" + apiKey) // 填充自定義 APIKey.header("X-DashScope-Async", "enable") // 讓用戶必需顯示開啟異步, 也方便后續擴展.header("Content-Type", "application/json").body(JSONUtil.toJsonStr(createOutPaintingTaskRequest));// 使用 Hutool 的 JSONUtil, 因為剛剛的請求使用了 @Alias// 7. 使用 try...with 方法釋放 httpRequest 的資源, 自動釋放資源的對象必須實現 AutoCloseable 接口try(HttpResponse httpResponse = httpRequest.execute()){// 8. 響應碼異常if(!httpResponse.isOk()){log.error("請求異常: {}", httpResponse.body());throw new BusinessException(ErrorCode.OPERATION_ERROR, "AI 擴圖失敗");}// 9. 將正常的響應體轉為 JSON 格式的創建請求的響應對象CreateOutPaintingTaskResponse createOutPaintingTaskResponse = JSONUtil.toBean(httpResponse.body(), CreateOutPaintingTaskResponse.class);// 10. 拿到響應對象后, 根據響應對象 code 是否有值, 進一步判斷擴圖是否成功if (createOutPaintingTaskResponse.getCode() != null){String errMessage = createOutPaintingTaskResponse.getMessage();log.error("請求異常: {}", errMessage);throw new BusinessException(ErrorCode.OPERATION_ERROR, "AI 擴圖失敗" + errMessage);}return createOutPaintingTaskResponse;}}/*** 查詢創建的任務** @param taskId 任務 ID* @return 查詢任務響應*/public GetOutPaintingTaskResponse getOutPaintingTask(String taskId){if(StrUtil.isBlank(taskId)){throw new BusinessException(ErrorCode.PARAMS_ERROR, "任務 ID 不能為空");}// 11. 填寫請求頭, 發送請求
// --header "Authorization: Bearer $DASHSCOPE_API_KEY" \
// https://dashscope.aliyuncs.com/api/v1/tasks/86ecf553-d340-4e21-xxxxxxxxxString url = String.format(GET_OUT_PAINTING_TASK_URL , taskId); // 注解 4 查詢請求的 URL , "%s" 替換為 taskIdtry(HttpResponse httpResponse = HttpRequest.get(url).header("Authorization", "Bearer" + apiKey) .execute()){// 響應碼異常if(!httpResponse.isOk()){log.error("請求異常: {}", httpResponse.body());throw new BusinessException(ErrorCode.OPERATION_ERROR, "獲取任務結果失敗");}return JSONUtil.toBean(httpResponse.body(), GetOutPaintingTaskResponse.class);}}
}
注意:要按照官方文檔的要求給請求頭增加鑒權信息,拼接配置中寫好的 apiKey
。
2. 開發擴圖 API 調用接口
(1) 數據模型開發
在 model.dto.picture
包下新建 AI 擴圖請求類,用于接受前端傳來的參數并傳遞給 Service 服務層。
字段包括圖片 id 和擴圖參數:
@Data
public class CreatePictureOutPaintingTaskRequest implements Serializable {/*** 圖片 id*/private Long pictureId;/*** 擴圖參數*/private CreateOutPaintingTaskRequest.Parameters parameters;private static final long serialVersionUID = 1L;
}
我們只需要傳一個已有的圖片,即可實現擴圖功能,具體流程:
- 前端構造
Parameters(內部類)
的各個參數,并與圖片 ID 一起構造擴圖請求; - 前端向后端發送
擴圖請求
; - 后端從請求中解析
圖片 ID
和圖像處理參數 Parameters
; - 查詢數據庫,找到
圖片 ID 對應的圖片
,并進行關于圖片與空間的鑒權
; - 將
圖片的 URL
和Parameters
作為參數,構造API 擴圖請求
,調用擴圖 API; - 擴圖 API 解析請求,校驗參數,創建擴圖任務;
- 創建的擴圖任務,放入
大模型擴圖任務隊列
中,并生成對應的taskId
返回; - 可以通過調用
查看進度 API
,查看對應 taskId 對應的生成進度
; - 擴圖成功后,
查看進度 API
會封裝URL
到響應中,返回給前端;
(2) 擴圖服務開發
在圖片服務中編寫創建擴圖任務方法,從數據庫中獲取圖片信息和 URL 地址,構造請求參數后調用 API 創建擴圖任務
。
注意,如果圖片有空間 id,則需要校驗權限,直接復用以前的權限校驗方法。
@Override
public void checkPictureAuth(User loginUser, Picture picture) {Long spaceId = picture.getSpaceId();Long loginUserId = loginUser.getId();if (spaceId == null) {// 公共圖庫, 僅本人和管理員可操作if (!picture.getUserId().equals(loginUserId) && !userService.isAdmin(loginUser)) {throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}} else {// 私有空間, 僅空間管理員可操作if (!picture.getUserId().equals(loginUserId)) {throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}}
}
在調用大模型接口前,先調用該方法,對圖片進行鑒權,只有空間管理員,可以對圖片調用擴圖 API
接下來,我們對調用大模型 API 進行服務開發:
/*** 創建擴圖任務* @param createPictureOutPaintingTaskRequest 擴圖請求* @param loginUser* @return*/
CreateOutPaintingTaskResponse createPictureOutPaintingTask(CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest, User loginUser);
@Resource
private AliYunAiApi aliYunAiApi;@Override
public CreateOutPaintingTaskResponse createPictureOutPaintingTask(CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest, User loginUser) {// 1. 根據請求獲取圖片 IDLong pictureId = createPictureOutPaintingTaskRequest.getPictureId();// 2. 查詢數據庫, 獲取圖片, 如果數據庫沒有該圖片, 拋異常
// Picture picture = this.getById(pictureId);
// ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR, "圖片不存在");Picture picture = Optional.ofNullable(this.getById(pictureId)).orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND_ERROR, "圖片不存在"));// Optional.ofNullable(...): 安全地包裝可能為 null 的查詢結果;// orElseThrow(...):如果結果確實為 null, 就立即拋出指定的異常;// 3. 校驗權限checkPictureAuth(loginUser, picture);// 4. 創建擴圖任務請求CreateOutPaintingTaskRequest createOutPaintingTaskRequest = new CreateOutPaintingTaskRequest();CreateOutPaintingTaskRequest.Input input = new CreateOutPaintingTaskRequest.Input();// 內部類 Input, 也作為參數input.setImageUrl(picture.getUrl());createOutPaintingTaskRequest.setInput(input);createOutPaintingTaskRequest.setParameters(createPictureOutPaintingTaskRequest.getParameters());// 5. 調用 API 創建任務return aliYunAiApi.createOutPaintingTask(createOutPaintingTaskRequest);
}
(3) 擴圖接口開發
在 PictureController
添加 AI 擴圖接口,包括創建任務和查詢任務狀態接口:
/*** 創建 AI 擴圖任務*/
@PostMapping("/out_painting/create_task")
public BaseResponse<CreateOutPaintingTaskResponse> createPictureOutPaintingTask(@RequestBody CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest,HttpServletRequest request) {if (createPictureOutPaintingTaskRequest == null || createPictureOutPaintingTaskRequest.getPictureId() == null) {throw new BusinessException(ErrorCode.PARAMS_ERROR);}User loginUser = userService.getLoginUser(request);CreateOutPaintingTaskResponse response = pictureService.createPictureOutPaintingTask(createPictureOutPaintingTaskRequest, loginUser);return ResultUtils.success(response);
}
@Resource
private AliYunAiApi aliYunAiApi;/*** 查詢 AI 擴圖任務*/
@GetMapping("/out_painting/get_task")
public BaseResponse<GetOutPaintingTaskResponse> getPictureOutPaintingTask(String taskId) {ThrowUtils.throwIf(StrUtil.isBlank(taskId), ErrorCode.PARAMS_ERROR);GetOutPaintingTaskResponse task = aliYunAiApi.getOutPaintingTask(taskId);return ResultUtils.success(task);
}
(4) 接口測試
測試圖片:
發送擴圖任務請求:
響應:
復制 taskId,調用查看任務接口:
打開 outputImageUrl
效果對比:
至此,我們的 AI 圖片編輯后端開發完成啦~~~~