Java AI智能P图工具技术笔记
一、项目概述
本项目是基于Java Swing开发的AI智能P图桌面工具,集成摄像头拍照、本地图片加载、字节跳动火山方舟AI图像生成接口调用功能,实现通过文本提示词对图片进行AI滤镜处理,核心技术栈:Java Swing(GUI)、Webcam Capture(摄像头)、Java HttpClient(网络请求)、Base64(图片编码)、火山方舟AI开放接口。
二、核心模块拆分与代码解析
1. AI服务调用模块(AIService.java)
负责本地图片Base64编码、构建AI接口请求、发送HTTP请求、解析响应结果、下载生成后的图片,是核心业务模块。
java
package aiimgpro;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.time.Duration;
import java.util.Base64;
// 智能P图服务调用接口
public class AIService {
// 火山方舟AI接口地址
private static final String API_URL = "https://ark.cn-beijing.volces.com/api/v3/images/generations";
// AI模型名称
private static final String MODEL = "doubao-seedream-5-0-260128";
// 接口认证密钥
private static final String apiKey = "ark-03de5193-3179-4250-b026-3dca2e9224d0-4c349";
/**
* 调用AI生成图片(阻塞方法,需在后台执行)
* @param imageFile 本地图片文件
* @param prompt 提示词
* @return 处理后的BufferedImage对象
* @throws Exception 网络/解析异常
*/
public BufferedImage generateImage(File imageFile, String prompt) throws Exception
{
// 1. 图片转Base64编码,适配AI接口要求
byte[] imageBytes = Files.readAllBytes(imageFile.toPath());
String base64Image = Base64.getEncoder().encodeToString(imageBytes);
String mimeType = detectMimeType(imageFile.getName());
// 拼接DataURI格式,AI接口标准入参
String dataUri = "data:" + mimeType + ";base64," + base64Image;
// 2. 构建JSON请求体
String jsonBody = buildRequestJson(prompt, dataUri);
// 3. 创建HTTP客户端,设置30秒连接超时
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30))
.build();
// 4. 构建POST请求,携带请求头和请求体
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
// Bearer认证,接口鉴权核心
.header("Authorization", "Bearer " + apiKey)
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
// 120秒请求超时,适配AI生成耗时
.timeout(Duration.ofSeconds(120))
.build();
// 5. 发送请求,获取字符串格式响应
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
int statusCode = response.statusCode();
String body = response.body();
// 6. 异常处理:非200状态码抛出错误
if (statusCode != 200) {
String errMsg = extractJsonValue(body, "message");
if (errMsg == null) errMsg = body;
throw new RuntimeException("API返回错误 (HTTP " + statusCode + "): " +
errMsg);
}
// 7. 解析响应,提取生成后的图片URL
String imageUrl = extractImageUrl(body);
if (imageUrl == null || imageUrl.isBlank()) {
throw new RuntimeException("未从API响应中获取到图片URL。\n响应内容: " +
body);
}
// 8. 下载生成的图片,返回流格式
HttpRequest imgRequest = HttpRequest.newBuilder()
.uri(URI.create(imageUrl))
.GET()
.timeout(Duration.ofSeconds(60))
.build();
HttpResponse<InputStream> imgResponse = client.send(imgRequest,
HttpResponse.BodyHandlers.ofInputStream());
// 9. 流转换为BufferedImage,返回给UI渲染
BufferedImage resultImage = ImageIO.read(imgResponse.body());
if (resultImage == null) {
throw new RuntimeException("无法解码API返回的图片。");
}
return resultImage;
}
// ======================== JSON工具方法 ========================
/**
* 手动构建请求JSON(无第三方依赖,轻量化)
*/
private String buildRequestJson(String prompt, String imageDataUri) {
StringBuilder sb = new StringBuilder();
sb.append("{");
sb.append("\"model\":\"").append(jsonEscape(MODEL)).append("\",");
sb.append("\"prompt\":\"").append(jsonEscape(prompt)).append("\",");
sb.append("\"image\":\"").append(jsonEscape(imageDataUri)).append("\",");
sb.append("\"size\":\"2K\","); // 图片分辨率
sb.append("\"sequential_image_generation\":\"disabled\",");
sb.append("\"response_format\":\"url\","); // 响应格式:URL
sb.append("\"stream\":false,"); // 关闭流式响应
sb.append("\"watermark\":true"); // 开启水印
sb.append("}");
return sb.toString();
}
/**
* JSON转义:处理特殊字符,避免请求格式错误
*/
private static String jsonEscape(String s) {
if (s == null) return "";
StringBuilder sb = new StringBuilder(s.length() + 16);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '"': sb.append("\\\""); break; // 转义双引号
case '\\': sb.append("\\\\"); break; // 转义反斜杠
case '\b': sb.append("\\b"); break;
case '\f': sb.append("\\f"); break;
case '\n': sb.append("\\n"); break; // 转义换行
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
default:
if (c < 0x20) {
sb.append(String.format("\\u%04x", (int) c));
} else {
sb.append(c);
}
}
}
return sb.toString();
}
/**
* 提取响应中的图片URL(手动解析,无第三方依赖)
*/
private static String extractImageUrl(String json) {
int dataIdx = json.indexOf("\"data\"");
if (dataIdx < 0) return null;
String sub = json.substring(dataIdx);
int urlIdx = sub.indexOf("\"url\"");
if (urlIdx < 0) return null;
sub = sub.substring(urlIdx + 5);
int colon = sub.indexOf(':');
if (colon < 0) return null;
sub = sub.substring(colon + 1).stripLeading();
if (sub.isEmpty() || sub.charAt(0) != '"') return null;
sub = sub.substring(1);
int endQuote = sub.indexOf('"');
if (endQuote < 0) return null;
return sub.substring(0, endQuote).replace("\\/", "/");
}
/**
* 通用JSON值提取方法
*/
private static String extractJsonValue(String json, String key) {
String search = "\"" + key + "\"";
int idx = json.indexOf(search);
if (idx < 0) return null;
String sub = json.substring(idx + search.length());
int colon = sub.indexOf(':');
if (colon < 0) return null;
sub = sub.substring(colon + 1).stripLeading();
if (sub.isEmpty()) return null;
if (sub.charAt(0) == '"') {
sub = sub.substring(1);
int end = sub.indexOf('"');
if (end < 0) return null;
return sub.substring(0, end);
}
int end = 0;
while (end < sub.length() && sub.charAt(end) != ',' && sub.charAt(end) != '}')
end++;
return sub.substring(0, end).strip();
}
/**
* 根据文件名后缀识别图片MIME类型
*/
private static String detectMimeType(String filename) {
String lower = filename.toLowerCase();
if (lower.endsWith(".png")) return "image/png";
if (lower.endsWith(".gif")) return "image/gif";
if (lower.endsWith(".bmp")) return "image/bmp";
if (lower.endsWith(".webp")) return "image/webp";
return "image/jpeg"; // 默认JPEG
}
}
2. 事件监听模块(AgentListener.java)
实现Swing按钮的事件监听,处理打开相机、拍照、打开图片、应用滤镜四大核心操作,衔接UI与AI服务。
java
package aiimgpro;
import com.github.sarxos.webcam.Webcam;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Date;
public class AgentListener implements ActionListener {
Graphics g; // 绘图对象,用于UI渲染图片
JTextField promptField; // 提示词输入框
File selectedFile; // 选中/拍摄的图片文件
AIService aiService = new AIService(); // AI服务实例
Webcam webcam; // 摄像头对象
String path="D:\\相册"; // 拍照保存路径
@Override
public void actionPerformed(ActionEvent e) {
String ac = e.getActionCommand();
// 分支判断按钮类型,执行对应逻辑
if (ac.equals("打开相机")) {
openCam();
} else if (ac.equals("拍照")) {
takePhoto();
} else if (ac.equals("打开图片")) {
openImage();
} else if (ac.equals("应用滤镜")) {
applyFilter();
}
}
/**
* 打开本地图片并渲染到UI
*/
public void openImage() {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("选择图片");
// 文件选择器确认选择
if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
selectedFile = chooser.getSelectedFile();
try {
// 读取图片并绘制到左侧区域
BufferedImage img = ImageIO.read(selectedFile);
g.drawImage(img, 30, 100, 500, 500, null);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/**
* 调用AI服务应用滤镜,渲染结果图片
*/
public void applyFilter() {
String prompt = promptField.getText();
// 非空校验
if (prompt.isEmpty() || selectedFile == null) {
JOptionPane.showMessageDialog(null, "请选择图片文件并输入提示词~");
} else {
try {
// 核心:调用AI接口获取处理后图片
BufferedImage img = aiService.generateImage(selectedFile, prompt);
// 绘制到右侧区域,对比展示原图与AI图
g.drawImage(img, 600, 100, 500, 500, null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**
* 打开摄像头,启动预览线程
*/
public void openCam() {
if (webcam != null&&webcam.isOpen()) {
return; // 避免重复打开
}
if(webcam==null) {
webcam = Webcam.getDefault(); // 获取默认摄像头
}
webcam.open();
// 启动独立线程实时渲染摄像头画面
CamThread camThread = new CamThread();
camThread.g = g;
camThread.webcam = webcam;
camThread.start();
}
/**
* 关闭摄像头
*/
public void closeCam() {
if (webcam.isOpen()) {
webcam.close();
}
}
/**
* 拍照并保存到本地,赋值给选中文件
*/
private void takePhoto() {
BufferedImage image = webcam.getImage();
g.drawImage(image, 30, 100, 300, 200, null);
// 时间戳命名,避免文件名重复
Date date = new Date();
String timeStr = date.getYear()+1900+"-"+(date.getMonth()+1)+"-"+date.getDate()
+"-"+date.getHours()+"-"+date.getMinutes()+"-"+date.getSeconds();
File imgeFile = new File(path+"\\"+timeStr+".png");
try {
ImageIO.write(image, "png", imgeFile); // 保存图片
} catch (IOException ex) {
throw new RuntimeException(ex);
}
selectedFile=imgeFile; // 保存文件用于AI处理
closeCam(); // 拍照后关闭相机
}
}
3. 摄像头预览线程模块(CamThread.java)
独立线程实时获取摄像头画面并渲染,避免阻塞UI主线程,保证界面流畅性。
java
package aiimgpro;
import com.github.sarxos.webcam.Webcam;
import java.awt.*;
import java.awt.image.BufferedImage;
// 摄像头预览线程
public class CamThread extends Thread{
Graphics g; // 绘图对象
Webcam webcam; // 摄像头对象
@Override
public void run() {
// 无限循环,实时渲染画面
while (true){
try {
Thread.sleep(100); // 100ms刷新一次,降低性能消耗
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 相机开启时渲染画面,关闭则终止线程
if(webcam.isOpen()){
BufferedImage image = webcam.getImage();
g.drawImage(image,30,100,300,200,null);
}else {
System.out.println("相机线程结束~");
break; // 退出循环,终止线程
}
}
}
}
4. UI界面模块(ImageAgentUI.java)
基于Swing搭建桌面图形界面,布局组件、注册事件监听,实现可视化操作。
java
package aiimgpro;
import javax.swing.*;
import java.awt.*;
// UI主界面
public class ImageAgentUI extends JFrame{
public ImageAgentUI() {
setTitle("Image Agent"); // 窗口标题
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 关闭窗口退出程序
setSize(1100, 600); // 窗口尺寸
setLayout(new FlowLayout()); // 流式布局
// 初始化功能按钮与输入框
JButton openCamBtn = new JButton("打开相机");
JButton takePhotoBtn = new JButton("拍照");
JButton openBtn = new JButton("打开图片");
JLabel promptLbl = new JLabel("滤镜提示词:");
JTextField promptTxt = new JTextField(20);
JButton applyBtn = new JButton("应用滤镜");
// 添加组件到窗口
add(openCamBtn);
add(takePhotoBtn);
add(openBtn);
add(promptLbl);
add(promptTxt);
add(applyBtn);
setVisible(true); // 显示窗口
Graphics g = getGraphics(); // 获取绘图对象
// 注册事件监听器,绑定组件
AgentListener listener = new AgentListener();
takePhotoBtn.addActionListener(listener);
openCamBtn.addActionListener(listener);
openBtn.addActionListener(listener);
applyBtn.addActionListener(listener);
// 传递参数给监听器
listener.promptField = promptTxt;
listener.g = g;
}
// 程序入口
public static void main(String[] args) {
new ImageAgentUI(); // 启动UI
}
}
三、关键技术点总结
- 图片Base64编码:将本地图片转为字符串格式,满足AI接口入参要求,是图片上传的核心步骤。
- 原生HttpClient请求:无第三方依赖,轻量级实现HTTP请求,适配AI接口的POST调用、超时控制、鉴权头配置。
- 手动JSON解析/构建:不依赖FastJSON/Gson,降低项目体积,通过字符串操作处理请求体和响应数据。
- 多线程优化:摄像头预览使用独立线程,避免UI卡顿;AI接口调用为阻塞方法,实际使用可优化为后台线程。
- Swing绘图机制 :通过
Graphics对象直接在窗口渲染原图、摄像头画面、AI生成图,实现实时预览。 - 接口鉴权 :使用
Bearer + API Key完成火山方舟接口认证,保证请求合法性。
四、注意事项
- 拍照保存路径
D:\\相册需提前创建,否则会报文件不存在异常; - AI接口调用为阻塞操作,建议添加加载提示,避免界面无响应;
- 摄像头依赖
webcam-capture第三方库,需正确导入依赖; - API Key为敏感信息,正式项目需加密存储,不可硬编码;
- 图片尺寸、生成参数可根据AI接口文档灵活调整。