【慧游鲁博】【11】后端 · Spring Boot 集成 Python 配置

发布于:2025-06-12 ⋅ 阅读:(19) ⋅ 点赞:(0)


关于在本机运行后端程序时,在 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 解释器

  1. 安装 Python 插件
    • File > Settings > Plugins,搜索 “Python”,安装 JetBrains 官方插件。
    • 重启 IDEA。
  2. 添加 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模型的图像匹配服务,主要功能是:

  1. 加载预训练的 CLIP 模型
  2. 计算输入图像的嵌入向量(embedding)
  3. 在数据库中查找与输入图像最相似的记录
  4. 返回匹配结果和相似度分数
工作流程
  1. 程序启动时加载 CLIP 模型
  2. 从命令行获取输入图像 URL 和数据库路径
  3. 下载输入图像并计算其嵌入向量
  4. 从数据库加载所有记录
  5. 对每条记录计算:
    • 输入图像与记录文本嵌入的相似度
    • 输入图像与记录图像嵌入的相似度
    • 加权综合评分 (默认文本权重 0.2,图像权重 0.8)
  6. 返回评分最高的记录

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 作为请求参数,尝试识别该图片对应的文物并给出识别结果