|
|
|
|
@ -0,0 +1,962 @@
|
|
|
|
|
package cn.iocoder.yudao.module.annotation.service.yolo;
|
|
|
|
|
|
|
|
|
|
import cn.iocoder.yudao.module.annotation.dal.dataobject.trainresult.TrainResultDO;
|
|
|
|
|
import cn.iocoder.yudao.module.annotation.dal.yolo.YoloConfig;
|
|
|
|
|
import cn.iocoder.yudao.module.annotation.service.python.PythonVirtualEnvService;
|
|
|
|
|
import cn.iocoder.yudao.module.annotation.service.python.vo.PythonVirtualEnvInfo;
|
|
|
|
|
import cn.iocoder.yudao.module.annotation.service.traininfo.TrainInfoService;
|
|
|
|
|
import cn.iocoder.yudao.module.annotation.service.trainresult.TrainResultService;
|
|
|
|
|
import jakarta.annotation.Resource;
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
|
|
import java.io.BufferedReader;
|
|
|
|
|
import java.io.File;
|
|
|
|
|
import java.io.InputStreamReader;
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
import java.util.concurrent.CompletableFuture;
|
|
|
|
|
import java.util.concurrent.Executor;
|
|
|
|
|
import java.util.concurrent.Executors;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* YOLO操作服务实现类
|
|
|
|
|
*
|
|
|
|
|
* @author 管理员
|
|
|
|
|
*/
|
|
|
|
|
@Slf4j
|
|
|
|
|
@Service
|
|
|
|
|
public class YoloOperationServiceImpl implements YoloOperationService {
|
|
|
|
|
|
|
|
|
|
@Resource
|
|
|
|
|
private PythonVirtualEnvService pythonVirtualEnvService;
|
|
|
|
|
|
|
|
|
|
@Resource
|
|
|
|
|
private TrainInfoService trainInfoService;
|
|
|
|
|
|
|
|
|
|
@Resource
|
|
|
|
|
private TrainResultService trainResultService;
|
|
|
|
|
|
|
|
|
|
// 异步执行器
|
|
|
|
|
private final Executor asyncExecutor = Executors.newCachedThreadPool();
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public CompletableFuture<YoloConfig> trainAsync(String pythonPath, String envName, YoloConfig config) {
|
|
|
|
|
return CompletableFuture.supplyAsync(() -> {
|
|
|
|
|
config.setStatus("running");
|
|
|
|
|
config.setProgress(0);
|
|
|
|
|
config.setTaskId(UUID.randomUUID().toString());
|
|
|
|
|
|
|
|
|
|
// 生成训练ID
|
|
|
|
|
Integer trainId = generateTrainId(config);
|
|
|
|
|
|
|
|
|
|
// 记录上一轮的epoch,用于检测epoch完成
|
|
|
|
|
int[] lastSavedEpoch = {0};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 检测虚拟环境
|
|
|
|
|
PythonVirtualEnvInfo envInfo = pythonVirtualEnvService.activateVirtualEnv(pythonPath, envName);
|
|
|
|
|
if (!envInfo.getVirtualEnvExists() || !envInfo.getYoloInstalled()) {
|
|
|
|
|
config.setStatus("failed");
|
|
|
|
|
config.setErrorMessage("虚拟环境不存在或未安装YOLO");
|
|
|
|
|
return config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建训练命令
|
|
|
|
|
String command = buildTrainCommand(envInfo, config);
|
|
|
|
|
|
|
|
|
|
log.info("开始YOLO训练,命令: {}", command);
|
|
|
|
|
|
|
|
|
|
// 执行训练
|
|
|
|
|
ProcessBuilder processBuilder = new ProcessBuilder();
|
|
|
|
|
// 在Windows上使用cmd,在Linux/Mac上使用bash
|
|
|
|
|
String os = System.getProperty("os.name").toLowerCase();
|
|
|
|
|
if (os.contains("win")) {
|
|
|
|
|
processBuilder.command("cmd", "/c", command);
|
|
|
|
|
// 设置环境变量确保正确处理中文路径
|
|
|
|
|
processBuilder.environment().put("PYTHONIOENCODING", "utf-8");
|
|
|
|
|
processBuilder.environment().put("LANG", "zh_CN.UTF-8");
|
|
|
|
|
} else {
|
|
|
|
|
processBuilder.command("/bin/bash", "-c", command);
|
|
|
|
|
processBuilder.environment().put("PYTHONIOENCODING", "utf-8");
|
|
|
|
|
}
|
|
|
|
|
Process process = processBuilder.start();
|
|
|
|
|
BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
|
|
|
|
BufferedReader stderrReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
|
|
|
|
|
|
|
|
|
|
StringBuilder fullOutput = new StringBuilder();
|
|
|
|
|
String line;
|
|
|
|
|
|
|
|
|
|
// 同时读取标准输出和错误输出
|
|
|
|
|
String stdoutLine, stderrLine = null;
|
|
|
|
|
while ((stdoutLine = stdoutReader.readLine()) != null || (stderrLine = stderrReader.readLine()) != null) {
|
|
|
|
|
if (stdoutLine != null) {
|
|
|
|
|
log.info("YOLO训练日志: {}", stdoutLine);
|
|
|
|
|
fullOutput.append(stdoutLine).append("\n");
|
|
|
|
|
|
|
|
|
|
// 详细解析YOLO训练进度和结果
|
|
|
|
|
parseTrainingProgress(stdoutLine, config, trainId, lastSavedEpoch);
|
|
|
|
|
|
|
|
|
|
config.setLogMessage(stdoutLine);
|
|
|
|
|
}
|
|
|
|
|
if (stderrLine != null) {
|
|
|
|
|
log.info("YOLO训练日志: {}", stderrLine);
|
|
|
|
|
fullOutput.append(stderrLine).append("\n");
|
|
|
|
|
|
|
|
|
|
// 详细解析YOLO训练进度和结果
|
|
|
|
|
parseTrainingProgress(stderrLine, config, trainId, lastSavedEpoch);
|
|
|
|
|
|
|
|
|
|
config.setLogMessage(stderrLine);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int exitCode = process.waitFor();
|
|
|
|
|
stdoutReader.close();
|
|
|
|
|
stderrReader.close();
|
|
|
|
|
|
|
|
|
|
if (exitCode == 0) {
|
|
|
|
|
config.setStatus("completed");
|
|
|
|
|
config.setProgress(100);
|
|
|
|
|
config.setLogMessage("训练完成");
|
|
|
|
|
// 解析最终训练结果摘要
|
|
|
|
|
parseFinalResults(fullOutput.toString(), config);
|
|
|
|
|
// 保存最终训练结果
|
|
|
|
|
saveFinalTrainResult(trainId, config, fullOutput.toString());
|
|
|
|
|
} else {
|
|
|
|
|
config.setStatus("failed");
|
|
|
|
|
config.setErrorMessage("训练失败,退出码: " + exitCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.error("YOLO训练异常", e);
|
|
|
|
|
config.setStatus("failed");
|
|
|
|
|
config.setErrorMessage("训练异常: " + e.getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return config;
|
|
|
|
|
}, asyncExecutor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public YoloConfig validate(String pythonPath, String envName, YoloConfig config) {
|
|
|
|
|
try {
|
|
|
|
|
PythonVirtualEnvInfo envInfo = pythonVirtualEnvService.activateVirtualEnv(pythonPath, envName);
|
|
|
|
|
if (!envInfo.getVirtualEnvExists() || !envInfo.getYoloInstalled()) {
|
|
|
|
|
config.setStatus("failed");
|
|
|
|
|
config.setErrorMessage("虚拟环境不存在或未安装YOLO");
|
|
|
|
|
return config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String command = buildValidateCommand(envInfo, config);
|
|
|
|
|
log.info("开始YOLO验证,命令: {}", command);
|
|
|
|
|
|
|
|
|
|
ProcessBuilder processBuilder = new ProcessBuilder();
|
|
|
|
|
String os = System.getProperty("os.name").toLowerCase();
|
|
|
|
|
if (os.contains("win")) {
|
|
|
|
|
processBuilder.command("cmd", "/c", command);
|
|
|
|
|
// 设置环境变量确保正确处理中文路径
|
|
|
|
|
processBuilder.environment().put("PYTHONIOENCODING", "utf-8");
|
|
|
|
|
processBuilder.environment().put("LANG", "zh_CN.UTF-8");
|
|
|
|
|
} else {
|
|
|
|
|
processBuilder.command("/bin/bash", "-c", command);
|
|
|
|
|
processBuilder.environment().put("PYTHONIOENCODING", "utf-8");
|
|
|
|
|
}
|
|
|
|
|
Process process = processBuilder.start();
|
|
|
|
|
BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
|
|
|
|
BufferedReader stderrReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
|
|
|
|
|
|
|
|
|
|
StringBuilder output = new StringBuilder();
|
|
|
|
|
String line;
|
|
|
|
|
|
|
|
|
|
// 同时读取标准输出和错误输出
|
|
|
|
|
String stdoutLine, stderrLine = null;
|
|
|
|
|
while ((stdoutLine = stdoutReader.readLine()) != null || (stderrLine = stderrReader.readLine()) != null) {
|
|
|
|
|
if (stdoutLine != null) {
|
|
|
|
|
output.append(stdoutLine).append("\n");
|
|
|
|
|
log.info("YOLO验证日志: {}", stdoutLine);
|
|
|
|
|
}
|
|
|
|
|
if (stderrLine != null) {
|
|
|
|
|
output.append(stderrLine).append("\n");
|
|
|
|
|
log.info("YOLO验证日志: {}", stderrLine);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int exitCode = process.waitFor();
|
|
|
|
|
stdoutReader.close();
|
|
|
|
|
stderrReader.close();
|
|
|
|
|
|
|
|
|
|
if (exitCode == 0) {
|
|
|
|
|
config.setStatus("completed");
|
|
|
|
|
config.setLogMessage("验证完成");
|
|
|
|
|
config.setMetrics(output.toString());
|
|
|
|
|
} else {
|
|
|
|
|
config.setStatus("failed");
|
|
|
|
|
config.setErrorMessage("验证失败,退出码: " + exitCode);
|
|
|
|
|
config.setLogMessage(output.toString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.error("YOLO验证异常", e);
|
|
|
|
|
config.setStatus("failed");
|
|
|
|
|
config.setErrorMessage("验证异常: " + e.getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public YoloConfig predict(String pythonPath, String envName, YoloConfig config) {
|
|
|
|
|
try {
|
|
|
|
|
PythonVirtualEnvInfo envInfo = pythonVirtualEnvService.activateVirtualEnv(pythonPath, envName);
|
|
|
|
|
if (!envInfo.getVirtualEnvExists() || !envInfo.getYoloInstalled()) {
|
|
|
|
|
config.setStatus("failed");
|
|
|
|
|
config.setErrorMessage("虚拟环境不存在或未安装YOLO");
|
|
|
|
|
return config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String command = buildPredictCommand(envInfo, config);
|
|
|
|
|
log.info("开始YOLO预测,命令: {}", command);
|
|
|
|
|
|
|
|
|
|
ProcessBuilder processBuilder = new ProcessBuilder();
|
|
|
|
|
String os = System.getProperty("os.name").toLowerCase();
|
|
|
|
|
if (os.contains("win")) {
|
|
|
|
|
processBuilder.command("cmd", "/c", command);
|
|
|
|
|
// 设置环境变量确保正确处理中文路径
|
|
|
|
|
processBuilder.environment().put("PYTHONIOENCODING", "utf-8");
|
|
|
|
|
processBuilder.environment().put("LANG", "zh_CN.UTF-8");
|
|
|
|
|
} else {
|
|
|
|
|
processBuilder.command("/bin/bash", "-c", command);
|
|
|
|
|
processBuilder.environment().put("PYTHONIOENCODING", "utf-8");
|
|
|
|
|
}
|
|
|
|
|
Process process = processBuilder.start();
|
|
|
|
|
BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
|
|
|
|
BufferedReader stderrReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
|
|
|
|
|
|
|
|
|
|
StringBuilder output = new StringBuilder();
|
|
|
|
|
String line;
|
|
|
|
|
|
|
|
|
|
// 同时读取标准输出和错误输出
|
|
|
|
|
String stdoutLine, stderrLine = null;
|
|
|
|
|
while ((stdoutLine = stdoutReader.readLine()) != null || (stderrLine = stderrReader.readLine()) != null) {
|
|
|
|
|
if (stdoutLine != null) {
|
|
|
|
|
output.append(stdoutLine).append("\n");
|
|
|
|
|
log.info("YOLO预测日志: {}", stdoutLine);
|
|
|
|
|
}
|
|
|
|
|
if (stderrLine != null) {
|
|
|
|
|
output.append(stderrLine).append("\n");
|
|
|
|
|
log.info("YOLO预测日志: {}", stderrLine);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int exitCode = process.waitFor();
|
|
|
|
|
stdoutReader.close();
|
|
|
|
|
stderrReader.close();
|
|
|
|
|
|
|
|
|
|
if (exitCode == 0) {
|
|
|
|
|
config.setStatus("completed");
|
|
|
|
|
config.setLogMessage("预测完成");
|
|
|
|
|
config.setMetrics(output.toString());
|
|
|
|
|
} else {
|
|
|
|
|
config.setStatus("failed");
|
|
|
|
|
config.setErrorMessage("预测失败,退出码: " + exitCode);
|
|
|
|
|
config.setLogMessage(output.toString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.error("YOLO预测异常", e);
|
|
|
|
|
config.setStatus("failed");
|
|
|
|
|
config.setErrorMessage("预测异常: " + e.getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public YoloConfig classify(String pythonPath, String envName, YoloConfig config) {
|
|
|
|
|
config.setTaskType("classify");
|
|
|
|
|
return predict(pythonPath, envName, config); // 分类和预测使用相同的逻辑,只是任务类型不同
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public YoloConfig export(String pythonPath, String envName, YoloConfig config) {
|
|
|
|
|
try {
|
|
|
|
|
PythonVirtualEnvInfo envInfo = pythonVirtualEnvService.activateVirtualEnv(pythonPath, envName);
|
|
|
|
|
if (!envInfo.getVirtualEnvExists() || !envInfo.getYoloInstalled()) {
|
|
|
|
|
config.setStatus("failed");
|
|
|
|
|
config.setErrorMessage("虚拟环境不存在或未安装YOLO");
|
|
|
|
|
return config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String command = buildExportCommand(envInfo, config);
|
|
|
|
|
log.info("开始YOLO导出,命令: {}", command);
|
|
|
|
|
|
|
|
|
|
ProcessBuilder processBuilder = new ProcessBuilder();
|
|
|
|
|
String os = System.getProperty("os.name").toLowerCase();
|
|
|
|
|
if (os.contains("win")) {
|
|
|
|
|
processBuilder.command("cmd", "/c", command);
|
|
|
|
|
// 设置环境变量确保正确处理中文路径
|
|
|
|
|
processBuilder.environment().put("PYTHONIOENCODING", "utf-8");
|
|
|
|
|
processBuilder.environment().put("LANG", "zh_CN.UTF-8");
|
|
|
|
|
} else {
|
|
|
|
|
processBuilder.command("/bin/bash", "-c", command);
|
|
|
|
|
processBuilder.environment().put("PYTHONIOENCODING", "utf-8");
|
|
|
|
|
}
|
|
|
|
|
Process process = processBuilder.start();
|
|
|
|
|
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
|
|
|
|
|
|
|
|
|
|
StringBuilder output = new StringBuilder();
|
|
|
|
|
String line;
|
|
|
|
|
while ((line = reader.readLine()) != null) {
|
|
|
|
|
output.append(line).append("\n");
|
|
|
|
|
log.info("YOLO导出日志: {}", line);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int exitCode = process.waitFor();
|
|
|
|
|
reader.close();
|
|
|
|
|
|
|
|
|
|
if (exitCode == 0) {
|
|
|
|
|
config.setStatus("completed");
|
|
|
|
|
config.setLogMessage("导出完成");
|
|
|
|
|
config.setMetrics(output.toString());
|
|
|
|
|
} else {
|
|
|
|
|
config.setStatus("failed");
|
|
|
|
|
config.setErrorMessage("导出失败,退出码: " + exitCode);
|
|
|
|
|
config.setLogMessage(output.toString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.error("YOLO导出异常", e);
|
|
|
|
|
config.setStatus("failed");
|
|
|
|
|
config.setErrorMessage("导出异常: " + e.getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public YoloConfig track(String pythonPath, String envName, YoloConfig config) {
|
|
|
|
|
try {
|
|
|
|
|
PythonVirtualEnvInfo envInfo = pythonVirtualEnvService.activateVirtualEnv(pythonPath, envName);
|
|
|
|
|
if (!envInfo.getVirtualEnvExists() || !envInfo.getYoloInstalled()) {
|
|
|
|
|
config.setStatus("failed");
|
|
|
|
|
config.setErrorMessage("虚拟环境不存在或未安装YOLO");
|
|
|
|
|
return config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String command = buildTrackCommand(envInfo, config);
|
|
|
|
|
log.info("开始YOLO追踪,命令: {}", command);
|
|
|
|
|
|
|
|
|
|
ProcessBuilder processBuilder = new ProcessBuilder();
|
|
|
|
|
String os = System.getProperty("os.name").toLowerCase();
|
|
|
|
|
if (os.contains("win")) {
|
|
|
|
|
processBuilder.command("cmd", "/c", command);
|
|
|
|
|
// 设置环境变量确保正确处理中文路径
|
|
|
|
|
processBuilder.environment().put("PYTHONIOENCODING", "utf-8");
|
|
|
|
|
processBuilder.environment().put("LANG", "zh_CN.UTF-8");
|
|
|
|
|
} else {
|
|
|
|
|
processBuilder.command("/bin/bash", "-c", command);
|
|
|
|
|
processBuilder.environment().put("PYTHONIOENCODING", "utf-8");
|
|
|
|
|
}
|
|
|
|
|
Process process = processBuilder.start();
|
|
|
|
|
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
|
|
|
|
|
|
|
|
|
|
StringBuilder output = new StringBuilder();
|
|
|
|
|
String line;
|
|
|
|
|
while ((line = reader.readLine()) != null) {
|
|
|
|
|
output.append(line).append("\n");
|
|
|
|
|
log.info("YOLO追踪日志: {}", line);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int exitCode = process.waitFor();
|
|
|
|
|
reader.close();
|
|
|
|
|
|
|
|
|
|
if (exitCode == 0) {
|
|
|
|
|
config.setStatus("completed");
|
|
|
|
|
config.setLogMessage("追踪完成");
|
|
|
|
|
config.setMetrics(output.toString());
|
|
|
|
|
} else {
|
|
|
|
|
config.setStatus("failed");
|
|
|
|
|
config.setErrorMessage("追踪失败,退出码: " + exitCode);
|
|
|
|
|
config.setLogMessage(output.toString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.error("YOLO追踪异常", e);
|
|
|
|
|
config.setStatus("failed");
|
|
|
|
|
config.setErrorMessage("追踪异常: " + e.getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String buildTrainCommand(PythonVirtualEnvInfo envInfo, YoloConfig config) {
|
|
|
|
|
StringBuilder command = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
if ("conda".equals(envInfo.getVirtualEnvType())) {
|
|
|
|
|
command.append("conda run -n ").append(new File(envInfo.getVirtualEnvPath()).getName()).append(" ");
|
|
|
|
|
} else {
|
|
|
|
|
command.append(envInfo.getVirtualEnvPythonPath()).append(" ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 规范化路径,处理双斜杠和中文路径问题
|
|
|
|
|
String datasetPath = normalizePath(config.getDatasetPath());
|
|
|
|
|
String outputPath = normalizePath(config.getOutputPath());
|
|
|
|
|
String modelPath = config.getModelPath() != null ? normalizePath(config.getModelPath()) : null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
command.append("-c \"");
|
|
|
|
|
command.append("import sys; sys.path.insert(0, '.'); ");
|
|
|
|
|
command.append("from ultralytics import YOLO; ");
|
|
|
|
|
|
|
|
|
|
if (modelPath != null && !modelPath.trim().isEmpty()) {
|
|
|
|
|
command.append("model = YOLO(r'").append(escapePythonString(modelPath)).append("'); ");
|
|
|
|
|
} else {
|
|
|
|
|
command.append("model = YOLO('").append(config.getModelName()).append("'); ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 使用raw string来处理中文路径
|
|
|
|
|
command.append("model.train(data=r'").append(escapePythonString(datasetPath)).append("', ");
|
|
|
|
|
command.append("epochs=").append(config.getEpochs()).append(", ");
|
|
|
|
|
command.append("batch=").append(config.getBatchSize()).append(", ");
|
|
|
|
|
command.append("imgsz=").append(config.getImageSize()).append(", ");
|
|
|
|
|
|
|
|
|
|
// 取消注释设备和学习率配置
|
|
|
|
|
// command.append("device=").append(config.getDevice()).append(", ");
|
|
|
|
|
// command.append("lr0=").append(config.getLearningRate()).append(", ");
|
|
|
|
|
// command.append("workers=").append(config.getWorkers()).append(", ");
|
|
|
|
|
//
|
|
|
|
|
if (config.getSavePeriod() > 0) {
|
|
|
|
|
command.append("save_period=").append(config.getSavePeriod()).append(", ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
command.append("project=r'").append(escapePythonString(outputPath)).append("')\"");
|
|
|
|
|
|
|
|
|
|
return command.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String buildValidateCommand(PythonVirtualEnvInfo envInfo, YoloConfig config) {
|
|
|
|
|
StringBuilder command = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
if ("conda".equals(envInfo.getVirtualEnvType())) {
|
|
|
|
|
command.append("conda run -n ").append(new File(envInfo.getVirtualEnvPath()).getName()).append(" ");
|
|
|
|
|
} else {
|
|
|
|
|
command.append(envInfo.getVirtualEnvPythonPath()).append(" ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
String datasetPath = normalizePath(config.getValDatasetPath() != null ?
|
|
|
|
|
config.getValDatasetPath() : config.getDatasetPath());
|
|
|
|
|
String modelPath = normalizePath(config.getModelPath());
|
|
|
|
|
|
|
|
|
|
command.append("-c \"");
|
|
|
|
|
command.append("import sys; sys.path.insert(0, '.'); ");
|
|
|
|
|
command.append("from ultralytics import YOLO; ");
|
|
|
|
|
command.append("model = YOLO(r'").append(escapePythonString(modelPath)).append("'); ");
|
|
|
|
|
command.append("results = model.val(data=r'").append(escapePythonString(datasetPath)).append("', ");
|
|
|
|
|
command.append("conf=").append(config.getConfThresh()).append(", ");
|
|
|
|
|
command.append("iou=").append(config.getIouThresh()).append(", ");
|
|
|
|
|
command.append("imgsz=").append(config.getImageSize()).append(", ");
|
|
|
|
|
command.append("device='").append(config.getDevice()).append("')\"");
|
|
|
|
|
|
|
|
|
|
return command.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String buildPredictCommand(PythonVirtualEnvInfo envInfo, YoloConfig config) {
|
|
|
|
|
StringBuilder command = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
if ("conda".equals(envInfo.getVirtualEnvType())) {
|
|
|
|
|
command.append("conda run -n ").append(new File(envInfo.getVirtualEnvPath()).getName()).append(" ");
|
|
|
|
|
} else {
|
|
|
|
|
command.append(envInfo.getVirtualEnvPythonPath()).append(" ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
String inputPath = normalizePath(config.getInputPath());
|
|
|
|
|
String modelPath = normalizePath(config.getModelPath());
|
|
|
|
|
|
|
|
|
|
command.append("-c \"");
|
|
|
|
|
command.append("import sys; sys.path.insert(0, '.'); ");
|
|
|
|
|
command.append("from ultralytics import YOLO; ");
|
|
|
|
|
command.append("model = YOLO(r'").append(escapePythonString(modelPath)).append("'); ");
|
|
|
|
|
command.append("results = model.predict(source=r'").append(escapePythonString(inputPath)).append("', ");
|
|
|
|
|
command.append("conf=").append(config.getConfThresh()).append(", ");
|
|
|
|
|
command.append("iou=").append(config.getIouThresh()).append(", ");
|
|
|
|
|
command.append("imgsz=").append(config.getImageSize()).append(", ");
|
|
|
|
|
command.append("device='").append(config.getDevice()).append("', ");
|
|
|
|
|
command.append("save=").append(config.isSave()).append(", ");
|
|
|
|
|
command.append("show=").append(config.isShow()).append(", ");
|
|
|
|
|
command.append("save_txt=").append(config.isSaveTxt()).append(", ");
|
|
|
|
|
command.append("save_crop=").append(config.isSaveCrops()).append(")\"");
|
|
|
|
|
|
|
|
|
|
return command.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String buildExportCommand(PythonVirtualEnvInfo envInfo, YoloConfig config) {
|
|
|
|
|
StringBuilder command = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
if ("conda".equals(envInfo.getVirtualEnvType())) {
|
|
|
|
|
command.append("conda run -n ").append(new File(envInfo.getVirtualEnvPath()).getName()).append(" ");
|
|
|
|
|
} else {
|
|
|
|
|
command.append(envInfo.getVirtualEnvPythonPath()).append(" ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
command.append("-c \"");
|
|
|
|
|
command.append("from ultralytics import YOLO; ");
|
|
|
|
|
command.append("model = YOLO('").append(config.getModelPath()).append("'); ");
|
|
|
|
|
command.append("model.export(format='").append(config.getFormat()).append("', ");
|
|
|
|
|
command.append("imgsz=").append(config.getImgsz()).append(", ");
|
|
|
|
|
command.append("optimize=").append(config.isOptimize()).append(")\"");
|
|
|
|
|
|
|
|
|
|
return command.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String buildTrackCommand(PythonVirtualEnvInfo envInfo, YoloConfig config) {
|
|
|
|
|
StringBuilder command = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
if ("conda".equals(envInfo.getVirtualEnvType())) {
|
|
|
|
|
command.append("conda run -n ").append(new File(envInfo.getVirtualEnvPath()).getName()).append(" ");
|
|
|
|
|
} else {
|
|
|
|
|
command.append(envInfo.getVirtualEnvPythonPath()).append(" ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
command.append("-c \"");
|
|
|
|
|
command.append("from ultralytics import YOLO; ");
|
|
|
|
|
command.append("model = YOLO('").append(config.getModelPath()).append("'); ");
|
|
|
|
|
command.append("results = model.track(source='").append(config.getInputPath()).append("', ");
|
|
|
|
|
command.append("conf=").append(config.getTrackConf()).append(", ");
|
|
|
|
|
command.append("iou=").append(config.getTrackIou()).append(", ");
|
|
|
|
|
command.append("tracker='").append(config.getTracker()).append("', ");
|
|
|
|
|
command.append("device='").append(config.getDevice()).append("', ");
|
|
|
|
|
command.append("save=").append(config.isSave()).append(")\"");
|
|
|
|
|
|
|
|
|
|
return command.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 解析训练进度详细信息
|
|
|
|
|
*/
|
|
|
|
|
private void parseTrainingProgress(String line, YoloConfig config, Integer trainId, int[] lastSavedEpoch) {
|
|
|
|
|
try {
|
|
|
|
|
String trimmedLine = line.trim();
|
|
|
|
|
|
|
|
|
|
// 解析Epoch信息: "Epoch 1/100" 或 "1/100"
|
|
|
|
|
if (trimmedLine.contains("Epoch") || (trimmedLine.matches("\\d+/\\d+"))) {
|
|
|
|
|
String[] parts = trimmedLine.split("\\s+");
|
|
|
|
|
for (String part : parts) {
|
|
|
|
|
if (part.contains("/")) {
|
|
|
|
|
String[] epochParts = part.split("/");
|
|
|
|
|
if (epochParts.length == 2) {
|
|
|
|
|
int currentEpoch = Integer.parseInt(epochParts[0]);
|
|
|
|
|
int totalEpochs = Integer.parseInt(epochParts[1]);
|
|
|
|
|
config.setCurrentEpoch(currentEpoch);
|
|
|
|
|
// 计算总体进度:epoch进度 + batch进度
|
|
|
|
|
int epochProgress = (int) ((double) currentEpoch / totalEpochs * 90); // 90%用于epoch,10%用于最终处理
|
|
|
|
|
int totalProgress = Math.min(epochProgress + (config.getBatchProgress() / 10), 95);
|
|
|
|
|
config.setProgress(totalProgress);
|
|
|
|
|
|
|
|
|
|
// 当epoch完成时保存训练信息
|
|
|
|
|
if (currentEpoch > lastSavedEpoch[0] && currentEpoch <= config.getEpochs()) {
|
|
|
|
|
saveTrainEpochInfo(trainId, currentEpoch, config.getEpochs(), config);
|
|
|
|
|
lastSavedEpoch[0] = currentEpoch;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析批次信息: "1/100" 或 "batch 1/100"
|
|
|
|
|
if (trimmedLine.contains("batch") || trimmedLine.matches("\\d+/\\d+.*loss")) {
|
|
|
|
|
if (trimmedLine.contains("batch")) {
|
|
|
|
|
String[] parts = trimmedLine.split("\\s+");
|
|
|
|
|
for (int i = 0; i < parts.length; i++) {
|
|
|
|
|
if (parts[i].equals("batch") && i + 1 < parts.length && parts[i + 1].contains("/")) {
|
|
|
|
|
String[] batchParts = parts[i + 1].split("/");
|
|
|
|
|
if (batchParts.length == 2) {
|
|
|
|
|
int currentBatch = Integer.parseInt(batchParts[0]);
|
|
|
|
|
int totalBatches = Integer.parseInt(batchParts[1]);
|
|
|
|
|
config.setCurrentBatch(currentBatch);
|
|
|
|
|
config.setTotalBatches(totalBatches);
|
|
|
|
|
config.setBatchProgress((int) ((double) currentBatch / totalBatches * 100));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析损失值: "loss: 0.1234", "box_loss: 0.1", "cls_loss: 0.05", "obj_loss: 0.02"
|
|
|
|
|
if (trimmedLine.contains("loss")) {
|
|
|
|
|
String[] lossParts = trimmedLine.split(",");
|
|
|
|
|
for (String lossPart : lossParts) {
|
|
|
|
|
lossPart = lossPart.trim();
|
|
|
|
|
if (lossPart.startsWith("loss:")) {
|
|
|
|
|
String value = lossPart.substring(5).trim();
|
|
|
|
|
config.setLoss(Double.parseDouble(value));
|
|
|
|
|
} else if (lossPart.startsWith("box_loss:")) {
|
|
|
|
|
// 可以添加更详细的损失记录
|
|
|
|
|
} else if (lossPart.startsWith("cls_loss:")) {
|
|
|
|
|
// 可以添加更详细的损失记录
|
|
|
|
|
} else if (lossPart.startsWith("obj_loss:")) {
|
|
|
|
|
// 可以添加更详细的损失记录
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析训练指标: "metrics/precision(B): 0.85", "metrics/recall(B): 0.78", "metrics/mAP50(B): 0.82", "metrics/mAP50-95(B): 0.65"
|
|
|
|
|
if (trimmedLine.contains("metrics/")) {
|
|
|
|
|
String[] metricParts = trimmedLine.split(",");
|
|
|
|
|
for (String metricPart : metricParts) {
|
|
|
|
|
metricPart = metricPart.trim();
|
|
|
|
|
if (metricPart.startsWith("metrics/precision")) {
|
|
|
|
|
String value = metricPart.split(":")[1].trim();
|
|
|
|
|
config.setPrecision(Double.parseDouble(value));
|
|
|
|
|
} else if (metricPart.startsWith("metrics/recall")) {
|
|
|
|
|
String value = metricPart.split(":")[1].trim();
|
|
|
|
|
config.setRecall(Double.parseDouble(value));
|
|
|
|
|
} else if (metricPart.startsWith("metrics/mAP50-95") || metricPart.startsWith("metrics/mAP")) {
|
|
|
|
|
String value = metricPart.split(":")[1].trim();
|
|
|
|
|
config.setMap(Double.parseDouble(value));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析学习率: "lr: 0.001"
|
|
|
|
|
if (trimmedLine.contains("lr:")) {
|
|
|
|
|
// 可以记录学习率变化
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.debug("解析训练进度时出错: {}", line, e);
|
|
|
|
|
// 忽略解析错误,继续处理下一行
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 解析最终训练结果
|
|
|
|
|
*/
|
|
|
|
|
private void parseFinalResults(String fullOutput, YoloConfig config) {
|
|
|
|
|
try {
|
|
|
|
|
String[] lines = fullOutput.split("\n");
|
|
|
|
|
StringBuilder summary = new StringBuilder();
|
|
|
|
|
summary.append("训练完成!最终结果:\n");
|
|
|
|
|
|
|
|
|
|
boolean foundResults = false;
|
|
|
|
|
|
|
|
|
|
for (String line : lines) {
|
|
|
|
|
String trimmedLine = line.trim();
|
|
|
|
|
|
|
|
|
|
// 查找最终的验证结果
|
|
|
|
|
if (trimmedLine.contains("Class") || trimmedLine.contains("Images") ||
|
|
|
|
|
trimmedLine.contains("Instances") || trimmedLine.contains("Box")) {
|
|
|
|
|
// 这些是表格标题,跳过
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 查找最终的mAP结果
|
|
|
|
|
if (trimmedLine.contains("mAP50") || trimmedLine.contains("mAP50-95")) {
|
|
|
|
|
summary.append("• ").append(trimmedLine).append("\n");
|
|
|
|
|
foundResults = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 查找精度和召回率
|
|
|
|
|
if (trimmedLine.contains("precision:") || trimmedLine.contains("recall:")) {
|
|
|
|
|
summary.append("• ").append(trimmedLine).append("\n");
|
|
|
|
|
foundResults = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 查找模型保存路径
|
|
|
|
|
if (trimmedLine.contains("Results saved to") || trimmedLine.contains("Saved to")) {
|
|
|
|
|
summary.append("• ").append(trimmedLine).append("\n");
|
|
|
|
|
foundResults = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 查找训练时间
|
|
|
|
|
if (trimmedLine.contains("Training completed in") || trimmedLine.contains("Time")) {
|
|
|
|
|
summary.append("• ").append(trimmedLine).append("\n");
|
|
|
|
|
foundResults = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!foundResults) {
|
|
|
|
|
summary.append("详细结果请查看日志输出和训练输出目录。\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加训练参数摘要
|
|
|
|
|
summary.append("\n训练参数摘要:\n");
|
|
|
|
|
summary.append(String.format("• 模型: %s\n", config.getModelName()));
|
|
|
|
|
summary.append(String.format("• 训练轮数: %d\n", config.getEpochs()));
|
|
|
|
|
summary.append(String.format("• 批次大小: %d\n", config.getBatchSize()));
|
|
|
|
|
summary.append(String.format("• 学习率: %.4f\n", config.getLearningRate()));
|
|
|
|
|
summary.append(String.format("• 图像尺寸: %d\n", config.getImageSize()));
|
|
|
|
|
summary.append(String.format("• 设备: %s\n", config.getDevice()));
|
|
|
|
|
|
|
|
|
|
// 如果解析到了指标,添加最终指标摘要
|
|
|
|
|
if (config.getPrecision() > 0 || config.getRecall() > 0 || config.getMap() > 0) {
|
|
|
|
|
summary.append("\n最终性能指标:\n");
|
|
|
|
|
if (config.getPrecision() > 0) {
|
|
|
|
|
summary.append(String.format("• 精度(Precision): %.4f\n", config.getPrecision()));
|
|
|
|
|
}
|
|
|
|
|
if (config.getRecall() > 0) {
|
|
|
|
|
summary.append(String.format("• 召回率(Recall): %.4f\n", config.getRecall()));
|
|
|
|
|
}
|
|
|
|
|
if (config.getMap() > 0) {
|
|
|
|
|
summary.append(String.format("• mAP: %.4f\n", config.getMap()));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
config.setTrainingSummary(summary.toString());
|
|
|
|
|
config.setMetrics(summary.toString());
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.error("解析最终训练结果时出错", e);
|
|
|
|
|
config.setTrainingSummary("训练完成,但解析详细结果时出错。请检查日志和输出文件。");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 生成训练ID
|
|
|
|
|
*/
|
|
|
|
|
private Integer generateTrainId(YoloConfig config) {
|
|
|
|
|
// 这里可以根据业务需求生成训练ID
|
|
|
|
|
// 简单实现:使用当前时间戳的后8位
|
|
|
|
|
return (int) (System.currentTimeMillis() % 100000000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 保存训练轮次信息
|
|
|
|
|
*/
|
|
|
|
|
private void saveTrainEpochInfo(Integer trainId, Integer currentEpoch, Integer totalEpochs, YoloConfig config) {
|
|
|
|
|
try {
|
|
|
|
|
// 构建训练信息
|
|
|
|
|
StringBuilder epochInfo = new StringBuilder();
|
|
|
|
|
epochInfo.append("Epoch ").append(currentEpoch).append("/").append(totalEpochs).append(" 完成\n");
|
|
|
|
|
|
|
|
|
|
if (config.getLoss() > 0) {
|
|
|
|
|
epochInfo.append("损失值: ").append(String.format("%.4f", config.getLoss())).append("\n");
|
|
|
|
|
}
|
|
|
|
|
if (config.getPrecision() > 0) {
|
|
|
|
|
epochInfo.append("精度: ").append(String.format("%.4f", config.getPrecision())).append("\n");
|
|
|
|
|
}
|
|
|
|
|
if (config.getRecall() > 0) {
|
|
|
|
|
epochInfo.append("召回率: ").append(String.format("%.4f", config.getRecall())).append("\n");
|
|
|
|
|
}
|
|
|
|
|
if (config.getMap() > 0) {
|
|
|
|
|
epochInfo.append("mAP: ").append(String.format("%.4f", config.getMap())).append("\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建识别率字符串
|
|
|
|
|
StringBuilder rateStr = new StringBuilder();
|
|
|
|
|
if (config.getPrecision() > 0) {
|
|
|
|
|
rateStr.append("precision:").append(String.format("%.4f", config.getPrecision()));
|
|
|
|
|
}
|
|
|
|
|
if (config.getRecall() > 0) {
|
|
|
|
|
if (rateStr.length() > 0) rateStr.append(",");
|
|
|
|
|
rateStr.append("recall:").append(String.format("%.4f", config.getRecall()));
|
|
|
|
|
}
|
|
|
|
|
if (config.getMap() > 0) {
|
|
|
|
|
if (rateStr.length() > 0) rateStr.append(",");
|
|
|
|
|
rateStr.append("map:").append(String.format("%.4f", config.getMap()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 保存到数据库
|
|
|
|
|
trainInfoService.saveTrainRoundInfo(
|
|
|
|
|
trainId,
|
|
|
|
|
currentEpoch,
|
|
|
|
|
totalEpochs,
|
|
|
|
|
epochInfo.toString(),
|
|
|
|
|
rateStr.toString()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
log.info("已保存训练轮次信息: trainId={}, epoch={}", trainId, currentEpoch);
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.error("保存训练轮次信息失败: trainId={}, epoch={}", trainId, currentEpoch, e);
|
|
|
|
|
// 不影响训练继续进行
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 保存最终训练结果
|
|
|
|
|
*/
|
|
|
|
|
private void saveFinalTrainResult(Integer trainId, YoloConfig config, String fullOutput) {
|
|
|
|
|
try {
|
|
|
|
|
// 构建结果路径(通常是训练输出目录)
|
|
|
|
|
String resultPath = config.getOutputPath();
|
|
|
|
|
if (resultPath == null || resultPath.trim().isEmpty()) {
|
|
|
|
|
resultPath = "runs/detect/train"; // YOLO默认输出路径
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建最终识别率
|
|
|
|
|
StringBuilder finalRate = new StringBuilder();
|
|
|
|
|
if (config.getPrecision() > 0) {
|
|
|
|
|
finalRate.append("precision:").append(String.format("%.4f", config.getPrecision()));
|
|
|
|
|
}
|
|
|
|
|
if (config.getRecall() > 0) {
|
|
|
|
|
if (finalRate.length() > 0) finalRate.append(",");
|
|
|
|
|
finalRate.append("recall:").append(String.format("%.4f", config.getRecall()));
|
|
|
|
|
}
|
|
|
|
|
if (config.getMap() > 0) {
|
|
|
|
|
if (finalRate.length() > 0) finalRate.append(",");
|
|
|
|
|
finalRate.append("map:").append(String.format("%.4f", config.getMap()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建TrainResultDO并保存
|
|
|
|
|
TrainResultDO trainResult = TrainResultDO.builder()
|
|
|
|
|
.trainId(trainId)
|
|
|
|
|
.path(resultPath)
|
|
|
|
|
.rate(finalRate.toString())
|
|
|
|
|
.dataId(null) // 可以根据需要设置数据集ID
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
// 由于TrainResultService可能没有直接的DO保存方法,我们可能需要使用VO
|
|
|
|
|
// 这里假设需要转换为SaveReqVO,如果没有这个方法,需要添加
|
|
|
|
|
saveTrainResultDO(trainResult);
|
|
|
|
|
|
|
|
|
|
log.info("已保存最终训练结果: trainId={}, path={}", trainId, resultPath);
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.error("保存最终训练结果失败: trainId={}", trainId, e);
|
|
|
|
|
// 不影响训练完成状态
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 保存TrainResultDO到数据库
|
|
|
|
|
*/
|
|
|
|
|
private void saveTrainResultDO(TrainResultDO trainResult) {
|
|
|
|
|
try {
|
|
|
|
|
// 创建SaveReqVO
|
|
|
|
|
cn.iocoder.yudao.module.annotation.controller.admin.trainresult.vo.TrainResultSaveReqVO saveReqVO =
|
|
|
|
|
new cn.iocoder.yudao.module.annotation.controller.admin.trainresult.vo.TrainResultSaveReqVO();
|
|
|
|
|
|
|
|
|
|
// 设置值
|
|
|
|
|
saveReqVO.setTrainId(trainResult.getTrainId());
|
|
|
|
|
saveReqVO.setPath(trainResult.getPath());
|
|
|
|
|
saveReqVO.setRate(trainResult.getRate());
|
|
|
|
|
saveReqVO.setDataId(trainResult.getDataId());
|
|
|
|
|
|
|
|
|
|
// 保存
|
|
|
|
|
trainResultService.createTrainResult(saveReqVO);
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.error("保存训练结果失败: {}", trainResult, e);
|
|
|
|
|
// 不影响训练完成状态
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 规范化路径,处理Windows/Linux路径分隔符、双斜杠等问题
|
|
|
|
|
*
|
|
|
|
|
* @param path 原始路径
|
|
|
|
|
* @return 规范化后的路径
|
|
|
|
|
*/
|
|
|
|
|
private String normalizePath(String path) {
|
|
|
|
|
if (path == null || path.trim().isEmpty()) {
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
String normalized = path.trim();
|
|
|
|
|
|
|
|
|
|
// 在Windows系统中,特别处理中文路径和特殊字符
|
|
|
|
|
String os = System.getProperty("os.name").toLowerCase();
|
|
|
|
|
if (os.contains("win")) {
|
|
|
|
|
// 对于Windows系统,需要确保路径能够正确处理中文
|
|
|
|
|
// 将反斜杠转换为正斜杠,但保持路径编码正确
|
|
|
|
|
normalized = normalized.replace("\\", "/");
|
|
|
|
|
|
|
|
|
|
// 处理双斜杠问题
|
|
|
|
|
while (normalized.contains("//")) {
|
|
|
|
|
normalized = normalized.replace("//", "/");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确保路径以正确的格式存在,避免额外的斜杠
|
|
|
|
|
if (normalized.startsWith("/") && normalized.length() > 1 && normalized.charAt(1) != '/') {
|
|
|
|
|
// 这是网络路径或绝对路径,保持原样
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
// 对于Linux/Mac系统,使用File类进行标准化
|
|
|
|
|
File file = new File(normalized);
|
|
|
|
|
normalized = file.getCanonicalPath();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.debug("路径规范化: {} -> {}", path, normalized);
|
|
|
|
|
return normalized;
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.warn("路径规范化失败,使用原始路径: {}", path, e);
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 转义Python字符串中的特殊字符
|
|
|
|
|
*
|
|
|
|
|
* @param input 原始字符串
|
|
|
|
|
* @return 转义后的字符串
|
|
|
|
|
*/
|
|
|
|
|
private String escapePythonString(String input) {
|
|
|
|
|
if (input == null) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 首先规范化路径
|
|
|
|
|
String normalized = normalizePath(input);
|
|
|
|
|
|
|
|
|
|
// 转义Python字符串中的特殊字符
|
|
|
|
|
StringBuilder escaped = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < normalized.length(); i++) {
|
|
|
|
|
char c = normalized.charAt(i);
|
|
|
|
|
|
|
|
|
|
switch (c) {
|
|
|
|
|
case '\\':
|
|
|
|
|
// 反斜杠需要转义
|
|
|
|
|
escaped.append("\\\\");
|
|
|
|
|
break;
|
|
|
|
|
case '"':
|
|
|
|
|
// 双引号需要转义(因为我们用双引号包围Python字符串)
|
|
|
|
|
escaped.append("\\\"");
|
|
|
|
|
break;
|
|
|
|
|
case '\'':
|
|
|
|
|
// 单引号也需要转义
|
|
|
|
|
escaped.append("\\\'");
|
|
|
|
|
break;
|
|
|
|
|
case '\n':
|
|
|
|
|
// 换行符
|
|
|
|
|
escaped.append("\\n");
|
|
|
|
|
break;
|
|
|
|
|
case '\r':
|
|
|
|
|
// 回车符
|
|
|
|
|
escaped.append("\\r");
|
|
|
|
|
break;
|
|
|
|
|
case '\t':
|
|
|
|
|
// 制表符
|
|
|
|
|
escaped.append("\\t");
|
|
|
|
|
break;
|
|
|
|
|
case '\b':
|
|
|
|
|
// 退格符
|
|
|
|
|
escaped.append("\\b");
|
|
|
|
|
break;
|
|
|
|
|
case '\f':
|
|
|
|
|
// 换页符
|
|
|
|
|
escaped.append("\\f");
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
// 对于其他控制字符(ASCII < 32),进行转义
|
|
|
|
|
if (c < 32) {
|
|
|
|
|
escaped.append(String.format("\\x%02x", (int) c));
|
|
|
|
|
} else {
|
|
|
|
|
escaped.append(c);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.debug("Python字符串转义: {} -> {}", input, escaped.toString());
|
|
|
|
|
return escaped.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|