文章目录
关于在本机运行后端程序时,在 Spring Boot 项目中集成 Python 脚本的环境说明
1. 为当前项目创建专属的 Python 环境
推荐使用conda
创建:
conda create --name 环境名 python=版本号
激活环境(Windows):
conda activate 环境名
激活后,终端提示符会显示环境名:
如图 “cxsx
”
从文件创建环境:
conda env create -f environment.yml
2. 安装缺失的模块
pip install -r requirements.txt
3. 在 IDEA 中配置 Python 解释器
- 安装 Python 插件:
File > Settings > Plugins
,搜索 “Python”,安装 JetBrains 官方插件。- 重启 IDEA。
- 添加 Python SDK:
File > Project Structure > Platform Settings > SDKs
。- 点击
+
,选择Python SDK
。 - 指定 Python 解释器路径(如
venv/bin/python
或系统 Python 路径)。
4. 集成 Python 脚本到 Spring Boot
修改文件如下:
4.1 创建Python服务封装
在resources目录下创建python_scripts/clip_service.py
:
# clip_service.py
import open_clip
import torch
from PIL import Image
import json
import numpy as np
from numpy.linalg import norm
import requests
from io import BytesIO
import sys
def load_model():
model, preprocess, _ = open_clip.create_model_and_transforms(
"ViT-B-32",
pretrained="laion2b_s34b_b79k"
)
device = "cuda" if torch.cuda.is_available() else "cpu"
return model.to(device).eval(), preprocess, device
model, preprocess, device = load_model()
def download_image(url):
response = requests.get(url)
img = Image.open(BytesIO(response.content)).convert("RGB")
return img
def get_image_embedding(image_url):
img = download_image(image_url)
tensor = preprocess(img).unsqueeze(0).to(device)
with torch.no_grad():
feat = model.encode_image(tensor)
feat = feat / feat.norm(dim=-1, keepdim=True)
return feat.cpu().numpy()[0]
def cosine(a, b):
return float(np.dot(a, b) / (norm(a) * norm(b)))
def match_image(image_url, db_path, w_text=0.2, w_image=0.8):
img_vec = get_image_embedding(image_url)
with open(db_path, "r", encoding="utf-8") as f:
records = json.load(f)
best = None
best_score = -1.0
for rec in records:
txt_vec = rec.get("text_embedding")
img_db_vec = rec.get("image_embedding")
if txt_vec is None or img_db_vec is None:
continue
sim_text = cosine(img_vec, np.array(txt_vec))
sim_img = cosine(img_vec, np.array(img_db_vec))
score = w_text * sim_text + w_image * sim_img
if score > best_score:
best_score = score
best = rec
return best, best_score
if __name__ == "__main__":
# 命令行调用示例: python clip_service.py <image_url> <db_path>
image_url = sys.argv[1]
db_path = sys.argv[2]
result, score = match_image(image_url, db_path)
print(json.dumps({
"result": result,
"score": score
}))
clip_service.py
程序是一个基于 CLIP模型的图像匹配服务,主要功能是:
- 加载预训练的 CLIP 模型
- 计算输入图像的嵌入向量(embedding)
- 在数据库中查找与输入图像最相似的记录
- 返回匹配结果和相似度分数
工作流程
- 程序启动时加载 CLIP 模型
- 从命令行获取输入图像 URL 和数据库路径
- 下载输入图像并计算其嵌入向量
- 从数据库加载所有记录
- 对每条记录计算:
- 输入图像与记录文本嵌入的相似度
- 输入图像与记录图像嵌入的相似度
- 加权综合评分 (默认文本权重 0.2,图像权重 0.8)
- 返回评分最高的记录
clip_service.py
脚本文件仅对队友提供的 python demo 文件image.py
做了微小修改,我主要是负责后续的将demo探索的功能集成到项目中,这也是本博客的目的
4.2 创建Java服务层
PythonService.java
public interface PythonService {
/**
* 根据图片URL匹配文物
* @param imageUrl 图片URL
* @return 匹配结果
*/
Result<ArtifactMatchResult> matchArtifact(String imageUrl);
}
PythonServiceImpl.java
@Service
@RequiredArgsConstructor
public class PythonServiceImpl implements PythonService {
private static final String PYTHON_SCRIPT_PATH = "src/main/resources/python_scripts/clip_service.py";
private static final String DB_PATH = "src/main/resources/python_scripts/shandong_museum_multimodal_embeddings.json";
@Override
public Result<ArtifactMatchResult> matchArtifact(String imageUrl) {
try {
String pythonPath = "E:\\anaconda\\envs\\cxsx\\python.exe"; // 注意转义反斜杠
ProcessBuilder pb = new ProcessBuilder(
pythonPath,
PYTHON_SCRIPT_PATH,
imageUrl,
DB_PATH
);
pb.redirectErrorStream(true);
Process process = pb.start();
// 读取Python脚本输出
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
StringBuilder output = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
output.append(line);
}
int exitCode = process.waitFor();
if (exitCode != 0) {
return Result.fail(ErrorCode.SYSTEM_ERROR, "Python脚本执行失败");
}
ArtifactMatchResult result = parseResult(output.toString());
if (result.getArtifact() == null) {
return Result.fail(ErrorCode.DATA_NOT_FOUND, "未找到匹配的文物");
}
return Result.success(result);
} catch (Exception e) {
return Result.fail(ErrorCode.SYSTEM_ERROR, "文物匹配失败: " + e.getMessage());
}
}
private ArtifactMatchResult parseResult(String jsonOutput) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(jsonOutput);
JsonNode resultNode = root.path("result");
double score = root.path("score").asDouble();
if (resultNode.isMissingNode()) {
return new ArtifactMatchResult(null, score);
}
Artifact artifact = mapper.treeToValue(resultNode, Artifact.class);
return new ArtifactMatchResult(artifact, score);
}
}
Artifact.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public
class Artifact {
private String name;
private String description;
private List<Double> text_embedding; // 改为下划线命名
private List<Double> image_embedding; // 改为下划线命名
}
ArtifactMatchResult.java
@Data
@AllArgsConstructor
public
class ArtifactMatchResult {
private Artifact artifact;
private double score;
}
这一系列Java程序实现了一个文物图片匹配服务,通过调用Python脚本进行图像匹配。下面我将详细讲解每个组件的功能和实现细节。
PythonService
接口
- 定义了服务层的接口,声明了文物匹配的方法
- 使用
Result
包装返回结果,便于统一处理成功/失败情况 - 参数为图片URL,返回匹配结果和相似度分数
PythonServiceImpl
实现类
- 通过调用Python脚本实现文物图片匹配
- 处理Python脚本的执行和结果解析
- 错误处理和结果包装
关键实现细节
1. 路径配置
private static final String PYTHON_SCRIPT_PATH = "src/main/resources/python_scripts/clip_service.py";
private static final String DB_PATH = "src/main/resources/python_scripts/shandong_museum_multimodal_embeddings.json";
- 定义了Python脚本路径和文物数据库路径
- 使用相对路径,需要注意项目部署时的路径问题
2. 执行Python脚本
String pythonPath = "E:\\anaconda\\envs\\cxsx\\python.exe";
ProcessBuilder pb = new ProcessBuilder(
pythonPath,
PYTHON_SCRIPT_PATH,
imageUrl,
DB_PATH
);
- 使用
ProcessBuilder
构建Python进程 - 需要指定Python解释器的完整路径(注意Windows下的反斜杠转义,这里我填入的是自己的项目环境中的解释器路径,大家记得替换一下)
- 传递图片URL和数据库路径作为参数
3. 处理脚本输出
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
StringBuilder output = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
output.append(line);
}
- 读取Python脚本的标准输出
- 将输出内容收集到StringBuilder中
4. 错误处理
int exitCode = process.waitFor();
if (exitCode != 0) {
return Result.fail(ErrorCode.SYSTEM_ERROR, "Python脚本执行失败");
}
- 检查Python脚本的退出码
- 非0退出码表示执行失败
5. 结果解析
private ArtifactMatchResult parseResult(String jsonOutput) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(jsonOutput);
JsonNode resultNode = root.path("result");
double score = root.path("score").asDouble();
if (resultNode.isMissingNode()) {
return new ArtifactMatchResult(null, score);
}
Artifact artifact = mapper.treeToValue(resultNode, Artifact.class);
return new ArtifactMatchResult(artifact, score);
}
- 使用Jackson库解析
JSON
输出 - 提取匹配分数和文物信息
- 处理文物不存在的情况
数据模型类Artifact.java
- 表示文物实体
- 包含名称、描述和两种嵌入向量
- 使用
Lombok
简化代码(自动生成getter/setter等) - 字段名与Python脚本输出保持一致(使用下划线)
ArtifactMatchResult.java
- 包装匹配结果
- 包含匹配到的文物和相似度分数
4.3 创建REST控制器
// ArtifactController.java
@RestController
@RequestMapping("/artifacts")
@RequiredArgsConstructor
public class ArtifactController {
private final PythonService pythonService;
@PostMapping("/match")
public Result<String> matchArtifact(@RequestBody MatchRequest request) {
Result<ArtifactMatchResult> result = pythonService.matchArtifact(request.getImageUrl());
String mode = request.getMode();
if (!result.isSuccess()) {
return Result.fail(result.getCode(), result.getMessage());
}
Artifact artifact = result.getData().getArtifact();
double score = result.getData().getScore();
// 构建基础文本
String responseText = String.format(
"识别结果: %s\n\n文物描述: %s\n\n匹配度: %.2f%%",
artifact.getName(),
artifact.getDescription(),
score * 100
);
return Result.success(responseText);
} catch (Exception e) {
return Result.fail(500, "图片识别出错: " + e.getMessage());
}
// DTO类
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class MatchRequest {
@NotBlank(message = "图片URL不能为空")
private String imageUrl;
}
}
接收小程序端的发来的 图片url
作为请求参数,尝试识别该图片对应的文物并给出识别结果