增加lx服务api接口

修改视觉模式,先数据集,后项目,再训练
master
LAPTOP-S9HJSOEB\昊天 2 weeks ago
parent 627fdfe833
commit 3c439b31e0

Binary file not shown.

@ -211,14 +211,14 @@ public class InvocableHandlerMethod extends HandlerMethod {
catch (InvocationTargetException ex) {
// Unwrap for HandlerExceptionResolvers ...
Throwable targetException = ex.getTargetException();
if (targetException instanceof RuntimeException runtimeException) {
throw runtimeException;
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
}
else if (targetException instanceof Error error) {
throw error;
else if (targetException instanceof Error) {
throw (Error) targetException;
}
else if (targetException instanceof Exception exception) {
throw exception;
else if (targetException instanceof Exception) {
throw (Exception) targetException;
}
else {
throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);

@ -15,4 +15,8 @@ public interface ErrorCodeConstants {
// ========== 训练结果模块 1-008-002-000 ==========
ErrorCode TRAIN_RESULT_NOT_EXISTS = new ErrorCode(1_008_002_000, "训练结果不存在");
// ========== 数据集模块 1-008-003-000 ==========
ErrorCode DATASET_NOT_EXISTS = new ErrorCode(1_008_003_000, "数据集不存在");
ErrorCode DATASET_PATH_NOT_SET = new ErrorCode(1_008_003_001, "数据集路径未设置");
}

@ -9,9 +9,12 @@ import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.annotation.controller.admin.datas.vo.DatasEnhance;
import cn.iocoder.yudao.module.annotation.controller.admin.datas.vo.DatasPageReqVO;
import cn.iocoder.yudao.module.annotation.controller.admin.datas.vo.DatasRespVO;
import cn.iocoder.yudao.module.annotation.controller.admin.datas.vo.DatasRespVO.DatasetInfo;
import cn.iocoder.yudao.module.annotation.controller.admin.datas.vo.DatasSaveReqVO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.datas.DatasDO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.dataset.DatasetDO;
import cn.iocoder.yudao.module.annotation.service.datas.DatasService;
import cn.iocoder.yudao.module.annotation.service.dataset.DatasetService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -24,7 +27,10 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@ -38,6 +44,9 @@ public class DatasController {
@Resource
private DatasService datasService;
@Resource
private DatasetService datasetService;
@PostMapping("/create")
@Operation(summary = "创建数据集管理")
@PreAuthorize("@ss.hasPermission('annotation:datas:create')")
@ -69,26 +78,31 @@ public class DatasController {
@PreAuthorize("@ss.hasPermission('annotation:datas:query')")
public CommonResult<DatasRespVO> getDatas(@RequestParam("id") Integer id) {
DatasDO datas = datasService.getDatas(id);
return success(BeanUtils.toBean(datas, DatasRespVO.class));
}
// 刷新数据集
/**
*
* 1.path
* @param id
* @return
*/
@GetMapping("/refreshDatas")
@Operation(summary = "获得数据集管理")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
public CommonResult<Boolean> refreshDatas(@RequestParam("id") Integer id) {
List<DatasDO> list = datasService.list(new LambdaQueryWrapper<DatasDO>().eq(DatasDO::getId, id));
for (DatasDO datasDO : list){
datasService.refreshDatas(datasDO);
DatasRespVO respVO = BeanUtils.toBean(datas, DatasRespVO.class);
// 填充数据集列表信息
if (datas.getDatasets() != null && !datas.getDatasets().isEmpty()) {
List<DatasetInfo> datasetList = new ArrayList<>();
String[] datasetIds = datas.getDatasets().split(",");
for (String datasetIdStr : datasetIds) {
try {
Integer datasetId = Integer.parseInt(datasetIdStr.trim());
DatasetDO dataset = datasetService.getDataset(datasetId);
if (dataset != null) {
DatasetInfo info = new DatasetInfo();
info.setId(dataset.getId());
info.setName(dataset.getName());
info.setPath(dataset.getPath());
datasetList.add(info);
}
} catch (NumberFormatException e) {
// 忽略无效的数据集ID
}
}
respVO.setDatasetList(datasetList);
}
return success(true);
return success(respVO);
}
@PostMapping("/list")
@ -106,20 +120,47 @@ public class DatasController {
List<DatasDO> result = datasService.list(new LambdaQueryWrapper<DatasDO>().eq(DatasDO::getStatus, pageReqVO.getStatus()));
return success(result);
}
///默认每张图片都生成一个新图片,循环遍历选择增强的类型
@PostMapping("/enhance")
@Operation(summary = "图片增强")
// @PreAuthorize("@ss.hasPermission('annotation:datas:query')")
public CommonResult<Boolean> enhance( @RequestBody DatasEnhance datasEnhance) {
@PreAuthorize("@ss.hasPermission('annotation:datas:query')")
public CommonResult<Boolean> enhance(@RequestBody DatasEnhance datasEnhance) {
datasService.enhance(datasEnhance);
return success(true);
}
@GetMapping("/page")
@Operation(summary = "获得数据集管理分页")
@PreAuthorize("@ss.hasPermission('annotation:datas:query')")
public CommonResult<PageResult<DatasRespVO>> getDatasPage(@Valid DatasPageReqVO pageReqVO) {
PageResult<DatasDO> pageResult = datasService.getDatasPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, DatasRespVO.class));
PageResult<DatasRespVO> respPageResult = BeanUtils.toBean(pageResult, DatasRespVO.class);
// 填充数据集列表信息
for (DatasRespVO respVO : respPageResult.getList()) {
if (respVO.getDatasets() != null && !respVO.getDatasets().isEmpty()) {
List<DatasetInfo> datasetList = new ArrayList<>();
String[] datasetIds = respVO.getDatasets().split(",");
for (String datasetIdStr : datasetIds) {
try {
Integer datasetId = Integer.parseInt(datasetIdStr.trim());
DatasetDO dataset = datasetService.getDataset(datasetId);
if (dataset != null) {
DatasetInfo info = new DatasetInfo();
info.setId(dataset.getId());
info.setName(dataset.getName());
info.setPath(dataset.getPath());
datasetList.add(info);
}
} catch (NumberFormatException e) {
// 忽略无效的数据集ID
}
}
respVO.setDatasetList(datasetList);
}
}
return success(respPageResult);
}
@GetMapping("/export-excel")

@ -1,12 +1,12 @@
package cn.iocoder.yudao.module.annotation.controller.admin.datas.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import java.util.*;
import org.springframework.format.annotation.DateTimeFormat;
import lombok.Data;
import java.time.LocalDateTime;
import com.alibaba.excel.annotation.*;
import java.util.List;
@Schema(description = "管理后台 - 数据集管理 Response VO")
@Data
@ -29,10 +29,6 @@ public class DatasRespVO {
@ExcelProperty("类型,还未标注,正在标注,正在训练,训练完成")
private String status;
@Schema(description = "路径")
@ExcelProperty("路径")
private String path;
@Schema(description = "图片总数", example = "4380")
@ExcelProperty("图片总数")
private Long count;
@ -49,4 +45,44 @@ public class DatasRespVO {
@ExcelProperty("创建时间")
private LocalDateTime createTime;
@Schema(description = "数据集ID列表逗号分隔")
private String datasets;
@Schema(description = "数据集列表")
private List<DatasetInfo> datasetList;
@Schema(description = "训练比例")
private Integer trainRatio;
@Schema(description = "验证比例")
private Integer valRatio;
@Schema(description = "测试比例")
private Integer testRatio;
@Schema(description = "训练次数")
private Integer epochs;
@Schema(description = "批次")
private Double batch;
@Schema(description = "图片大小")
private Integer imageSize;
@Schema(description = "模型路径")
private String modelPath;
@Data
@Schema(description = "数据集信息")
public static class DatasetInfo {
@Schema(description = "数据集ID")
private Integer id;
@Schema(description = "数据集名称")
private String name;
@Schema(description = "数据集路径")
private String path;
}
}

@ -1,9 +1,7 @@
package cn.iocoder.yudao.module.annotation.controller.admin.datas.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import jakarta.validation.constraints.*;
import lombok.Data;
@Schema(description = "管理后台 - 数据集管理新增/修改 Request VO")
@Data
@ -21,9 +19,6 @@ public class DatasSaveReqVO {
@Schema(description = "类型,还未标注,正在标注,正在训练,训练完成", example = "1")
private String status;
@Schema(description = "路径")
private String path;
@Schema(description = "图片总数", example = "4380")
private Long count;
@ -33,4 +28,28 @@ public class DatasSaveReqVO {
@Schema(description = "进度")
private Integer progress;
@Schema(description = "数据集ID列表逗号分隔")
private String datasets;
@Schema(description = "训练比例", example = "70")
private Integer trainRatio;
@Schema(description = "验证比例", example = "20")
private Integer valRatio;
@Schema(description = "测试比例", example = "10")
private Integer testRatio;
@Schema(description = "训练次数", example = "100")
private Integer epochs;
@Schema(description = "批次", example = "16.0")
private Double batch;
@Schema(description = "图片大小", example = "640")
private Integer imageSize;
@Schema(description = "模型路径")
private String modelPath;
}

@ -0,0 +1,102 @@
package cn.iocoder.yudao.module.annotation.controller.admin.dataset;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.annotation.controller.admin.dataset.vo.DatasetPageReqVO;
import cn.iocoder.yudao.module.annotation.controller.admin.dataset.vo.DatasetRespVO;
import cn.iocoder.yudao.module.annotation.controller.admin.dataset.vo.DatasetSaveReqVO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.dataset.DatasetDO;
import cn.iocoder.yudao.module.annotation.service.dataset.DatasetService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 识别数据集")
@RestController
@RequestMapping("/annotation/dataset")
@Validated
public class DatasetController {
@Resource
private DatasetService datasetService;
@PostMapping("/create")
@Operation(summary = "创建识别数据集")
@PreAuthorize("@ss.hasPermission('annotation:dataset:create')")
public CommonResult<Integer> createDataset(@Valid @RequestBody DatasetSaveReqVO createReqVO) {
return success(datasetService.createDataset(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新识别数据集")
@PreAuthorize("@ss.hasPermission('annotation:dataset:update')")
public CommonResult<Boolean> updateDataset(@Valid @RequestBody DatasetSaveReqVO updateReqVO) {
datasetService.updateDataset(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除识别数据集")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('annotation:dataset:delete')")
public CommonResult<Boolean> deleteDataset(@RequestParam("id") Integer id) {
datasetService.deleteDataset(id);
return success(true);
}
@PostMapping("/add-images")
@Operation(summary = "向数据集添加图片")
@PreAuthorize("@ss.hasPermission('annotation:dataset:update')")
public CommonResult<List<String>> addImages(@RequestBody DatasetSaveReqVO updateReqVO) {
List<String> imageUrls = datasetService.addImages(updateReqVO.getId(), updateReqVO.getFiles());
return success(imageUrls);
}
@GetMapping("/get")
@Operation(summary = "获得识别数据集")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('annotation:dataset:query')")
public CommonResult<DatasetRespVO> getDataset(@RequestParam("id") Integer id) {
DatasetDO dataset = datasetService.getDataset(id);
return success(BeanUtils.toBean(dataset, DatasetRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得识别数据集分页")
@PreAuthorize("@ss.hasPermission('annotation:dataset:query')")
public CommonResult<PageResult<DatasetRespVO>> getDatasetPage(@Valid DatasetPageReqVO pageReqVO) {
PageResult<DatasetDO> pageResult = datasetService.getDatasetPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, DatasetRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出识别数据集 Excel")
@PreAuthorize("@ss.hasPermission('annotation:dataset:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportDatasetExcel(@Valid DatasetPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<DatasetDO> list = datasetService.getDatasetPage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "识别数据集.xls", "数据", DatasetRespVO.class,
BeanUtils.toBean(list, DatasetRespVO.class));
}
}

@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.annotation.controller.admin.dataset.vo;
import lombok.*;
import java.util.*;
import io.swagger.v3.oas.annotations.media.Schema;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 识别数据集分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class DatasetPageReqVO extends PageParam {
@Schema(description = "名称", example = "芋艿")
private String name;
@Schema(description = "路径可以输入或者根据导入zip自动生成")
private String path;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.annotation.controller.admin.dataset.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import java.util.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import com.alibaba.excel.annotation.*;
@Schema(description = "管理后台 - 识别数据集 Response VO")
@Data
@ExcelIgnoreUnannotated
public class DatasetRespVO {
@Schema(description = "id", requiredMode = Schema.RequiredMode.REQUIRED, example = "21637")
@ExcelProperty("id")
private Integer id;
@Schema(description = "名称", example = "芋艿")
@ExcelProperty("名称")
private String name;
@Schema(description = "路径可以输入或者根据导入zip自动生成")
@ExcelProperty("路径可以输入或者根据导入zip自动生成")
private String path;
@Schema(description = "创建时间")
@ExcelProperty("创建时间")
private LocalDateTime createTime;
}

@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.annotation.controller.admin.dataset.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.constraints.*;
import java.util.*;
@Schema(description = "管理后台 - 识别数据集新增/修改 Request VO")
@Data
public class DatasetSaveReqVO {
@Schema(description = "id", example = "21637")
private Integer id;
@Schema(description = "名称", example = "芋艿")
private String name;
@Schema(description = "路径可以输入或者根据导入zip自动生成")
private String path;
@Schema(description = "上传的文件zip压缩包或多张图片")
private MultipartFile[] files;
}

@ -1,12 +1,7 @@
package cn.iocoder.yudao.module.annotation.controller.admin.mark;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.annotation.controller.admin.mark.vo.MarkPageReqVO;
import cn.iocoder.yudao.module.annotation.controller.admin.mark.vo.MarkRespVO;
import cn.iocoder.yudao.module.annotation.controller.admin.mark.vo.MarkSaveReqVO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.mark.MarkDO;
@ -18,16 +13,13 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 标注")
@ -95,25 +87,32 @@ public class MarkController {
return success(BeanUtils.toBean(mark, MarkRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得标注分页")
@PreAuthorize("@ss.hasPermission('annotation:mark:query')")
public CommonResult<PageResult<MarkRespVO>> getMarkPage(@Valid MarkPageReqVO pageReqVO) {
PageResult<MarkDO> pageResult = markService.getMarkPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, MarkRespVO.class));
@PutMapping("/update-file")
@Operation(summary = "更新标注文件")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('annotation:mark:update')")
public CommonResult<Boolean> updateAnnotationFile(@RequestParam("id") Integer id) {
markService.updateAnnotationFile(id);
return success(true);
}
@DeleteMapping("/delete-file")
@Operation(summary = "删除标注文件")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('annotation:mark:delete')")
public CommonResult<Boolean> deleteAnnotationFile(@RequestParam("id") Integer id) {
markService.deleteAnnotationFile(id);
return success(true);
}
@GetMapping("/export-excel")
@Operation(summary = "导出标注 Excel")
@PreAuthorize("@ss.hasPermission('annotation:mark:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportMarkExcel(@Valid MarkPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<MarkDO> list = markService.getMarkPage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "标注.xls", "数据", MarkRespVO.class,
BeanUtils.toBean(list, MarkRespVO.class));
@GetMapping("/read-file")
@Operation(summary = "读取标注文件内容")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('annotation:mark:query')")
public CommonResult<String> readAnnotationFile(@RequestParam("id") Integer id) {
String content = markService.readAnnotationFile(id);
return success(content);
}
}

@ -1,11 +1,16 @@
package cn.iocoder.yudao.module.annotation.controller.admin.mark;
import cn.hutool.core.io.FileUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.annotation.controller.admin.mark.vo.MarkSaveReqVO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.datas.DatasDO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.mark.MarkDO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.mark.MarkInfoDO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.types.TypesDO;
import cn.iocoder.yudao.module.annotation.service.MarkInfo.AnnotationData;
import cn.iocoder.yudao.module.annotation.service.MarkInfo.MarkInfoService;
import cn.iocoder.yudao.module.annotation.service.datas.DatasService;
import cn.iocoder.yudao.module.annotation.service.mark.MarkService;
import cn.iocoder.yudao.module.annotation.service.types.TypesService;
import cn.iocoder.yudao.module.system.service.dict.DictDataService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
@ -13,68 +18,586 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.util.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 标注详情")
@Slf4j
@RestController
@RequestMapping("/annotation/markInfo")
@Validated
public class MarkInfoController {
@Resource
private MarkInfoService markInfoService;
@Resource
private MarkService markService;
@Resource
private DatasService datasService;
@Resource
private TypesService typesService;
@Resource
private DictDataService dictDataService;
@PostMapping("/create")
@Operation(summary = "创建标注详情")
@PreAuthorize("@ss.hasPermission('annotation:mark:create')")
public CommonResult<Integer> createMark(@Valid @RequestBody List<MarkInfoDO> createReqVO) {
if (createReqVO == null || createReqVO.isEmpty()) {
return success(0);
}
for (MarkInfoDO markInfoDO : createReqVO){
markInfoDO.setCreator(null);
markInfoDO.setUpdater(null);
markInfoDO.setAnnotationDataString(markInfoDO.getAnnotationData().toString());
markInfoService.saveOrUpdate(markInfoDO);
Integer markId = createReqVO.get(0).getMarkId();
MarkDO mark = markService.getMark(markId);
if (mark == null) {
log.error("标注不存在: {}", markId);
return success(0);
}
DatasDO datas = datasService.getDatas(mark.getDataId());
if (datas == null) {
log.error("项目不存在: {}", mark.getDataId());
return success(0);
}
String projectType = datas.getType();
try {
// 直接更新标注文件
if ("1".equals(projectType)) {
updateDetectionAnnotationFile(mark, createReqVO);
} else if ("2".equals(projectType)) {
updateClassificationAnnotation(mark, createReqVO.get(0), datas);
}
// 更新标注状态
markService.updateMark(new MarkSaveReqVO()
.setId(markId)
.setStatus(1));
return success(1);
} catch (Exception e) {
log.error("创建标注详情失败: markId={}", markId, e);
return success(0);
}
markInfoService.remove(new QueryWrapper<MarkInfoDO>().eq("mark_id",createReqVO.get(0).getMarkId()).notIn("id",createReqVO.stream().map(MarkInfoDO::getId).toList()));
markService.updateMark(new MarkSaveReqVO()
.setId(createReqVO.get(0).getMarkId())
.setStatus(1));
return success(1);
}
@PostMapping("/list")
@Operation(summary = "获得标注详情列表")
@PreAuthorize("@ss.hasPermission('annotation:datas:query')")
public CommonResult<List<MarkInfoDO>> list(@RequestParam("markId") Integer markId) {
MarkDO mark = markService.getMark(markId);
if (mark == null) {
log.error("标注不存在: {}", markId);
return success(new ArrayList<>());
}
DatasDO datas = datasService.getDatas(mark.getDataId());
if (datas == null) {
log.error("项目不存在: {}", mark.getDataId());
return success(new ArrayList<>());
}
List<MarkInfoDO> result = markInfoService.list(new QueryWrapper<MarkInfoDO>()
.eq("mark_id",markId));
for (MarkInfoDO markInfoDO : result){
markInfoDO.setAnnotationData(AnnotationData.fromString(markInfoDO.getAnnotationDataString()));
String projectType = datas.getType();
// 在字典表的visual_type里面
try {
if ("1".equals(projectType)) {
// 检测任务:读取标注文件并解析
return success(readDetectionAnnotations(mark, datas));
} else if ("2".equals(projectType)) {
// 分类任务:返回类别信息
return success(readClassificationAnnotations(mark, datas));
}
} catch (Exception e) {
log.error("读取标注详情失败: markId={}", markId, e);
}
return success(result);
}
return success(new ArrayList<>());
}
@DeleteMapping("/delete")
@Operation(summary = "删除标注详情")
@Parameter(name = "id", description = "编号", required = true)
@Parameter(name = "markId", description = "标注编号", required = true)
@Parameter(name = "annotationId", description = "标注ID从1开始", required = true)
@PreAuthorize("@ss.hasPermission('annotation:mark:delete')")
public CommonResult<Boolean> deleteMark(@RequestParam("id") Integer id) {
markInfoService.deleteMarkInfo(id);
return success(true);
public CommonResult<Boolean> deleteMark(@RequestParam("markId") Integer markId,
@RequestParam("annotationId") Integer annotationId) {
MarkDO mark = markService.getMark(markId);
if (mark == null) {
log.error("标注不存在: {}", markId);
return success(false);
}
DatasDO datas = datasService.getDatas(mark.getDataId());
if (datas == null) {
log.error("项目不存在: {}", mark.getDataId());
return success(false);
}
String projectType = datas.getType();
try {
if ("1".equals(projectType)) {
// 检测任务:删除标注文件中的某一行
deleteDetectionAnnotation(mark, annotationId);
} else if ("2".equals(projectType)) {
// 分类任务:不支持删除单个标注,需要重新分类
log.warn("分类任务不支持删除单个标注");
return success(false);
}
return success(true);
} catch (Exception e) {
log.error("删除标注详情失败: markId={}, annotationId={}", markId, annotationId, e);
return success(false);
}
}
/**
*
*/
private List<MarkInfoDO> readDetectionAnnotations(MarkDO mark, DatasDO datas) throws Exception {
List<MarkInfoDO> result = new ArrayList<>();
// 获取标注文件路径
String labelPath = mark.getLabelPath();
if (labelPath == null || labelPath.isEmpty()) {
log.warn("标注文件路径为空: markId={}", mark.getId());
return result;
}
File labelFile = new File(labelPath);
if (!labelFile.exists()) {
log.warn("标注文件不存在: {}", labelPath);
return result;
}
// 读取标注文件内容
String content = FileUtil.readUtf8String(labelFile);
if (content == null || content.trim().isEmpty()) {
return result;
}
// 获取图片尺寸
String imagePath = mark.getImagePath();
if (imagePath == null || imagePath.isEmpty()) {
log.warn("图片路径为空: markId={}", mark.getId());
return result;
}
File imageFile = new File(imagePath);
if (!imageFile.exists()) {
log.warn("图片文件不存在: {}", imagePath);
return result;
}
BufferedImage image = ImageIO.read(imageFile);
int imageWidth = image.getWidth();
int imageHeight = image.getHeight();
// 获取类别映射
List<TypesDO> typesList = typesService.list(new QueryWrapper<TypesDO>()
.eq("data_id", mark.getDataId()));
typesList.sort(Comparator.comparing(TypesDO::getId));
// 解析标注文件
String[] lines = content.split("\n");
for (int i = 0; i < lines.length; i++) {
String line = lines[i].trim();
if (line.isEmpty()) {
continue;
}
try {
MarkInfoDO markInfo = parseYOLOLine(line, (long) (i + 1), imageWidth, imageHeight, typesList);
if (markInfo != null) {
markInfo.setMarkId(mark.getId());
markInfo.setDataId(mark.getDataId());
result.add(markInfo);
}
} catch (Exception e) {
log.error("解析YOLO标注行失败: {}", line, e);
}
}
return result;
}
/**
*
*/
private List<MarkInfoDO> readClassificationAnnotations(MarkDO mark, DatasDO datas) {
List<MarkInfoDO> result = new ArrayList<>();
// 从路径中提取类别名称
String path = mark.getImagePath();
if (path == null || path.isEmpty()) {
return result;
}
// 路径格式split/classname/imagename.jpg 或 split/classify/classname/imagename.jpg
String[] parts = path.split("[\\\\/]");
String className = null;
for (int i = parts.length - 1; i >= 0; i--) {
if (parts[i].equals("train") || parts[i].equals("val") || parts[i].equals("test")) {
className = parts[i + 1];
break;
}
}
if (className == null) {
log.warn("无法从路径中提取类别: {}", path);
return result;
}
// 查找对应的类别ID
List<TypesDO> typesList = typesService.list(new QueryWrapper<TypesDO>()
.eq("data_id", mark.getDataId())
.eq("name", className));
if (!typesList.isEmpty()) {
MarkInfoDO markInfo = new MarkInfoDO();
markInfo.setId(1L);
markInfo.setMarkId(mark.getId());
markInfo.setDataId(mark.getDataId());
markInfo.setClassId(typesList.get(0).getId());
markInfo.setClassName(typesList.get(0).getName());
// 创建一个空的AnnotationData用于兼容
AnnotationData emptyData = new AnnotationData();
markInfo.setAnnotationData(emptyData);
markInfo.setAnnotationDataString(emptyData.toString());
result.add(markInfo);
}
return result;
}
/**
* YOLO
*/
private MarkInfoDO parseYOLOLine(String line, Long id, int imageWidth, int imageHeight,
List<TypesDO> typesList) {
String[] parts = line.split("\\s+");
if (parts.length < 5) {
return null;
}
try {
int classIndex = Integer.parseInt(parts[0]);
double centerX = Double.parseDouble(parts[1]);
double centerY = Double.parseDouble(parts[2]);
double width = Double.parseDouble(parts[3]);
double height = Double.parseDouble(parts[4]);
// 转换为绝对坐标
double x = centerX * imageWidth - (width * imageWidth) / 2;
double y = centerY * imageHeight - (height * imageHeight) / 2;
double w = width * imageWidth;
double h = height * imageHeight;
// 获取类别ID
if (classIndex >= typesList.size()) {
log.warn("类别索引超出范围: {}", classIndex);
return null;
}
Integer classId = typesList.get(classIndex).getId();
// 生成UUID
String uuid = UUID.randomUUID().toString();
// 创建AnnotationData
AnnotationData annotationData = new AnnotationData();
annotationData.setId(uuid);
// 获取类别颜色并添加到bodies
String color = typesList.get(classIndex).getColor();
if (color != null && !color.isEmpty()) {
annotationData.setBodies(Arrays.asList(color));
} else {
annotationData.setBodies(Arrays.asList("#409EFF")); // 默认颜色
}
AnnotationData.Target target = new AnnotationData.Target();
target.setAnnotation(uuid);
AnnotationData.Target.Selector selector = new AnnotationData.Target.Selector();
selector.setType("RECTANGLE");
AnnotationData.Target.Selector.Geometry geometry = new AnnotationData.Target.Selector.Geometry();
geometry.setX(x);
geometry.setY(y);
geometry.setW(w);
geometry.setH(h);
// 计算bounds
AnnotationData.Target.Selector.Geometry.Bounds bounds = new AnnotationData.Target.Selector.Geometry.Bounds();
bounds.setMinX(x);
bounds.setMinY(y);
bounds.setMaxX(x + w);
bounds.setMaxY(y + h);
geometry.setBounds(bounds);
selector.setGeometry(geometry);
target.setSelector(selector);
// 创建creator
AnnotationData.Target.Creator creator = new AnnotationData.Target.Creator();
creator.setIsGuest(true);
creator.setId(UUID.randomUUID().toString().substring(0, 20)); // 生成20位随机ID
target.setCreator(creator);
// 创建时间戳
target.setCreated(new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
.format(new Date()));
annotationData.setTarget(target);
// 创建MarkInfoDO
MarkInfoDO markInfo = new MarkInfoDO();
markInfo.setId(id);
markInfo.setClassId(classId);
markInfo.setClassName(typesList.get(classIndex).getName());
markInfo.setCenterX(centerX);
markInfo.setCenterY(centerY);
markInfo.setWidth(width);
markInfo.setHeight(height);
markInfo.setAnnotationData(annotationData);
markInfo.setAnnotationDataString(annotationData.toString());
return markInfo;
} catch (Exception e) {
log.error("解析YOLO行失败: {}", line, e);
return null;
}
}
/**
*
*/
private void updateDetectionAnnotationFile(MarkDO mark, List<MarkInfoDO> markInfoList) throws Exception {
// 获取图片尺寸
String imagePath = mark.getImagePath();
if (imagePath == null || imagePath.isEmpty()) {
log.error("图片路径为空");
return;
}
File imageFile = new File(imagePath);
if (!imageFile.exists()) {
log.error("图片文件不存在: {}", imagePath);
return;
}
BufferedImage image = ImageIO.read(imageFile);
int imageWidth = image.getWidth();
int imageHeight = image.getHeight();
// 获取项目类型映射
List<TypesDO> typesList = typesService.list(new QueryWrapper<TypesDO>()
.eq("data_id", mark.getDataId()));
typesList.sort(Comparator.comparing(TypesDO::getId));
Map<Integer, Integer> classIdMap = new HashMap<>();
for (int i = 0; i < typesList.size(); i++) {
classIdMap.put(typesList.get(i).getId(), i);
}
// 确定标注文件路径
String labelPath = mark.getLabelPath();
if (labelPath == null || labelPath.isEmpty()) {
log.error("标注文件路径为空");
return;
}
File labelFile = new File(labelPath);
File labelDir = labelFile.getParentFile();
if (!labelDir.exists()) {
labelDir.mkdirs();
}
// 生成YOLO格式的标注文件
try (PrintWriter writer = new PrintWriter(new FileWriter(labelFile))) {
for (MarkInfoDO markInfo : markInfoList) {
Integer classIndex = classIdMap.get(markInfo.getClassId());
if (classIndex != null) {
AnnotationData annotationData = markInfo.getAnnotationData();
if (annotationData != null) {
String yoloLine = convertToYOLOFormat(annotationData, classIndex,
imageWidth, imageHeight);
if (yoloLine != null) {
writer.println(yoloLine);
}
}
}
}
}
log.info("更新检测标注文件成功: {}", labelFile.getAbsolutePath());
}
/**
*
*/
private void updateClassificationAnnotation(MarkDO mark, MarkInfoDO markInfo, DatasDO datas) throws Exception {
if (markInfo == null) {
log.error("标注信息为空");
return;
}
// 获取类别名称
TypesDO type = typesService.getById(markInfo.getClassId());
if (type == null) {
log.error("类别不存在: classId={}", markInfo.getClassId());
return;
}
String className = type.getName();
// 获取项目路径
// 源图片路径
String sourceImagePath = mark.getImagePath();
if (sourceImagePath == null || sourceImagePath.isEmpty()) {
log.error("源图片路径为空");
return;
}
File sourceImage = new File(sourceImagePath);
if (!sourceImage.exists()) {
log.error("源图片文件不存在: {}", sourceImagePath);
return;
}
// 源类型
// 路径格式split/classname/imagename.jpg 或 split/classify/classname/imagename.jpg
String[] parts = mark.getImagePath().split("[\\\\/]");
String splitType = null;
for (int i = parts.length - 1; i >= 0; i--) {
if (parts[i].equals("train") || parts[i].equals("val") || parts[i].equals("test")) {
splitType = parts[i + 1];
break;
}
}
// 确定目标目录split/classname/
String targetDirPath = mark.getImagePath().replace(splitType, markInfo.getClassName());
String relativePath = mark.getPath().replace(splitType, markInfo.getClassName());
File targetDir = new File(targetDirPath);
if (!targetDir.exists()) {
targetDir.mkdirs();
}
// 移动图片到目标目录
String imageFileName = sourceImage.getName();
File targetImage = new File(targetDir, imageFileName);
Files.move(sourceImage.toPath(), targetImage.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
markService.updateMark(new MarkSaveReqVO()
.setId(mark.getId())
.setPath(relativePath)
.setImagePath(targetDirPath));
log.info("更新分类标注成功: {}", targetImage.getAbsolutePath());
}
/**
*
*/
private void deleteDetectionAnnotation(MarkDO mark, Integer annotationId) throws Exception {
String labelPath = mark.getLabelPath();
if (labelPath == null || labelPath.isEmpty()) {
log.error("标注文件路径为空");
return;
}
File labelFile = new File(labelPath);
if (!labelFile.exists()) {
log.warn("标注文件不存在: {}", labelPath);
return;
}
// 读取标注文件内容
List<String> lines = FileUtil.readUtf8Lines(labelFile);
if (annotationId < 1 || annotationId > lines.size()) {
log.error("标注ID超出范围: {}", annotationId);
return;
}
// 删除指定行annotationId从1开始所以要减1
lines.remove(annotationId - 1);
// 写回文件
FileUtil.writeUtf8Lines(lines, labelFile);
log.info("删除检测标注成功: markId={}, annotationId={}", mark.getId(), annotationId);
}
/**
* train/val/test
*/
private String determineSplitType(String path) {
if (path == null || path.isEmpty()) {
return null;
}
String[] parts = path.split("/");
for (String part : parts) {
if (part.equals("train") || part.equals("val") || part.equals("test")) {
return part;
}
}
return "train"; // 默认train
}
/**
* YOLO
*/
private String convertToYOLOFormat(AnnotationData annotationData, Integer classIndex,
int imageWidth, int imageHeight) {
try {
AnnotationData.Target.Selector.Geometry geometry = annotationData.getTarget().getSelector().getGeometry();
// 获取边界框信息
double x = geometry.getX() != null ? geometry.getX() : 0;
double y = geometry.getY() != null ? geometry.getY() : 0;
double w = geometry.getW() != null ? geometry.getW() : 0;
double h = geometry.getH() != null ? geometry.getH() : 0;
// 转换为YOLO格式相对坐标和宽高
double centerX = (x + w / 2) / imageWidth;
double centerY = (y + h / 2) / imageHeight;
double width = w / imageWidth;
double height = h / imageHeight;
// 确保坐标在0-1范围内
centerX = Math.max(0, Math.min(1, centerX));
centerY = Math.max(0, Math.min(1, centerY));
width = Math.max(0, Math.min(1, width));
height = Math.max(0, Math.min(1, height));
return String.format("%d %.6f %.6f %.6f %.6f", classIndex, centerX, centerY, width, height);
} catch (Exception e) {
log.error("转换YOLO格式失败", e);
return null;
}
}
}

@ -9,5 +9,8 @@ public class MarkSaveReqVO {
private Integer dataId;
private Integer id;
private Integer status;
private String path;
private String imagePath;
private String labelPath;
}

@ -0,0 +1,95 @@
package cn.iocoder.yudao.module.annotation.controller.admin.model;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.constraints.*;
import jakarta.validation.*;
import jakarta.servlet.http.*;
import java.util.*;
import java.io.IOException;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
import cn.iocoder.yudao.module.annotation.controller.admin.model.vo.*;
import cn.iocoder.yudao.module.annotation.dal.dataobject.model.ModelDO;
import cn.iocoder.yudao.module.annotation.service.model.ModelService;
@Tag(name = "管理后台 - 模型管理")
@RestController
@RequestMapping("/annotation/model")
@Validated
public class ModelController {
@Resource
private ModelService modelService;
@PostMapping("/create")
@Operation(summary = "创建模型管理")
@PreAuthorize("@ss.hasPermission('annotation:model:create')")
public CommonResult<Integer> createModel(@Valid @RequestBody ModelSaveReqVO createReqVO) {
return success(modelService.createModel(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新模型管理")
@PreAuthorize("@ss.hasPermission('annotation:model:update')")
public CommonResult<Boolean> updateModel(@Valid @RequestBody ModelSaveReqVO updateReqVO) {
modelService.updateModel(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除模型管理")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('annotation:model:delete')")
public CommonResult<Boolean> deleteModel(@RequestParam("id") Integer id) {
modelService.deleteModel(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得模型管理")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('annotation:model:query')")
public CommonResult<ModelRespVO> getModel(@RequestParam("id") Integer id) {
ModelDO model = modelService.getModel(id);
return success(BeanUtils.toBean(model, ModelRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得模型管理分页")
@PreAuthorize("@ss.hasPermission('annotation:model:query')")
public CommonResult<PageResult<ModelRespVO>> getModelPage(@Valid ModelPageReqVO pageReqVO) {
PageResult<ModelDO> pageResult = modelService.getModelPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, ModelRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出模型管理 Excel")
@PreAuthorize("@ss.hasPermission('annotation:model:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportModelExcel(@Valid ModelPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<ModelDO> list = modelService.getModelPage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "模型管理.xls", "数据", ModelRespVO.class,
BeanUtils.toBean(list, ModelRespVO.class));
}
}

@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.annotation.controller.admin.model.vo;
import lombok.*;
import java.util.*;
import io.swagger.v3.oas.annotations.media.Schema;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 模型管理分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class ModelPageReqVO extends PageParam {
@Schema(description = "名称", example = "王五")
private String name;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
@Schema(description = "类型", example = "1")
private String type;
}

@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.annotation.controller.admin.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import java.util.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import com.alibaba.excel.annotation.*;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
@Schema(description = "管理后台 - 模型管理 Response VO")
@Data
@ExcelIgnoreUnannotated
public class ModelRespVO {
@Schema(description = "名称", example = "王五")
@ExcelProperty("名称")
private String name;
@Schema(description = "创建时间")
@ExcelProperty("创建时间")
private LocalDateTime createTime;
@Schema(description = "数据集列表")
@ExcelProperty("数据集列表")
private String datasets;
@Schema(description = "类型", example = "1")
@ExcelProperty(value = "类型", converter = DictConvert.class)
@DictFormat("visual_type") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
private String type;
@Schema(description = "项目id", example = "31774")
@ExcelProperty("项目id")
private Integer dataId;
@Schema(description = "采用数据集")
@ExcelProperty("采用数据集")
private String datasetIds;
}

@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.annotation.controller.admin.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 模型管理新增/修改 Request VO")
@Data
public class ModelSaveReqVO {
@Schema(description = "id", example = "1")
private Integer id;
@Schema(description = "名称", example = "王五")
private String name;
}

@ -54,5 +54,38 @@ public class DatasDO extends BaseDO {
*
*/
private double progress;
/**
* ,
*/
private String datasets;
/**
*
*/
private Integer trainRatio;
/**
*
*/
private Integer valRatio;
/**
*
*/
private Integer testRatio;
/**
*
*/
private Integer epochs;
/**
*
*/
private Double batch = -1.0;
/**
*
*/
private Integer imageSize;
/**
*
*/
private String modelPath;
}

@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.annotation.dal.dataobject.dataset;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* DO
*
* @author
*/
@TableName("annotation_dataset")
@KeySequence("annotation_dataset_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DatasetDO extends BaseDO {
/**
* id
*/
@TableId
private Integer id;
/**
*
*/
private String name;
/**
* zip
*/
private String path;
}

@ -34,16 +34,10 @@ public class MarkDO extends BaseDO {
* id
*/
private Integer dataId;
/**
* [{}]class_idcenter_xcenter_ywidthheightpolygon_pointsangle
*/
// 在 MarkDO 中使用
/**
* [{}]class_idcenter_xcenter_ywidthheightpolygon_pointsangle
*/
// @TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class)
// private List<AnnotationItem> annotation;
private String imagePath;
private String labelPath;
/**
* 1.0
*/

@ -0,0 +1,61 @@
package cn.iocoder.yudao.module.annotation.dal.dataobject.model;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import org.glassfish.jaxb.core.v2.TODO;
/**
* DO
*
* @author
*/
@TableName("annotation_model")
@KeySequence("annotation_model_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ModelDO extends BaseDO {
/**
* id
*/
@TableId
private Integer id;
/**
*
*/
private String name;
/**
*
*/
private String datasets;
/**
*
*
* {@link TODO visual_type }
*/
private String type;
/**
* id
*/
private Integer dataId;
/**
*
*/
private String datasetIds;
/**
*
*/
private Integer isDefault;
/**
*
*/
private String path;
}

@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.annotation.dal.mysql.dataset;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.annotation.dal.dataobject.dataset.DatasetDO;
import org.apache.ibatis.annotations.Mapper;
import cn.iocoder.yudao.module.annotation.controller.admin.dataset.vo.*;
/**
* Mapper
*
* @author
*/
@Mapper
public interface DatasetMapper extends BaseMapperX<DatasetDO> {
default PageResult<DatasetDO> selectPage(DatasetPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<DatasetDO>()
.likeIfPresent(DatasetDO::getName, reqVO.getName())
.eqIfPresent(DatasetDO::getPath, reqVO.getPath())
.betweenIfPresent(DatasetDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(DatasetDO::getId));
}
}

@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.annotation.dal.mysql.model;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.annotation.dal.dataobject.model.ModelDO;
import org.apache.ibatis.annotations.Mapper;
import cn.iocoder.yudao.module.annotation.controller.admin.model.vo.*;
/**
* Mapper
*
* @author
*/
@Mapper
public interface ModelMapper extends BaseMapperX<ModelDO> {
default PageResult<ModelDO> selectPage(ModelPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<ModelDO>()
.likeIfPresent(ModelDO::getName, reqVO.getName())
.betweenIfPresent(ModelDO::getCreateTime, reqVO.getCreateTime())
.eqIfPresent(ModelDO::getType, reqVO.getType())
.orderByDesc(ModelDO::getId));
}
}

@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.annotation.enums;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
/**
* annotation
*
* annotation 使 1-008-000-000
*/
public interface ErrorCodeConstants {
// ========== 训练信息模块 1-008-001-000 ==========
ErrorCode TRAIN_INFO_NOT_EXISTS = new ErrorCode(1_008_001_000, "训练信息不存在");
// ========== 训练结果模块 1-008-002-000 ==========
ErrorCode TRAIN_RESULT_NOT_EXISTS = new ErrorCode(1_008_002_000, "训练结果不存在");
// ========== 数据集模块 1-008-003-000 ==========
ErrorCode DATASET_NOT_EXISTS = new ErrorCode(1_008_003_000, "数据集不存在");
ErrorCode DATASET_PATH_NOT_SET = new ErrorCode(1_008_003_001, "数据集路径未设置");
ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1_008_003_001, "模型不存在");
}

@ -5,26 +5,35 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.annotation.controller.admin.datas.vo.DatasEnhance;
import cn.iocoder.yudao.module.annotation.controller.admin.datas.vo.DatasPageReqVO;
import cn.iocoder.yudao.module.annotation.controller.admin.datas.vo.DatasSaveReqVO;
import cn.iocoder.yudao.module.annotation.controller.admin.types.vo.TypesSaveReqVO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.datas.DatasDO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.dataset.DatasetDO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.mark.MarkDO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.mark.MarkInfoDO;
import cn.iocoder.yudao.module.annotation.dal.mysql.datas.DatasMapper;
import cn.iocoder.yudao.module.annotation.dal.mysql.mark.MarkInfoMapper;
import cn.iocoder.yudao.module.annotation.service.ImageService;
import cn.iocoder.yudao.module.annotation.service.dataset.DatasetService;
import cn.iocoder.yudao.module.annotation.service.mark.MarkService;
import cn.iocoder.yudao.module.annotation.service.types.TypesService;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
import cn.iocoder.yudao.module.system.service.dict.DictDataService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
//import static cn.iocoder.yudao.module.annotation.enums.ErrorCodeConstants.*;
/**
@ -32,6 +41,7 @@ import java.util.stream.Collectors;
*
* @author
*/
@Slf4j
@Service
@Validated
public class DatasServiceImpl extends ServiceImpl<DatasMapper, DatasDO> implements DatasService{
@ -44,89 +54,43 @@ public class DatasServiceImpl extends ServiceImpl<DatasMapper, DatasDO> impleme
@Resource
private DictDataService dictDataService;
/**
*
* @param path
* @return
*/
public List<String> getImageNamesFromPath(String superiorPath) {
DictDataDO basePath = dictDataService.parseDictData("visual_annotation_conf","base_path");
String path = basePath.getValue() + superiorPath;
List<String> imageNames = new ArrayList<>();
@Resource
private DatasetService datasetService;
// 检查路径是否为空
if (path == null || path.isEmpty()) {
return imageNames;
}
@Resource
private MarkInfoMapper markInfoMapper;
File directory = new File(path);
File rootDirectory = directory; // 保存根目录引用
@Resource
private ImageService imageService;
// 检查路径是否存在且为目录
/**
*
* @param directory
* @return
*/
private List<String> getAllImageFiles(File directory) {
List<String> imageFiles = new ArrayList<>();
if (!directory.exists() || !directory.isDirectory()) {
return imageNames;
return imageFiles;
}
// 递归遍历目录,传递根目录用于计算相对路径
traverseDirectory(directory, rootDirectory, imageNames);
imageNames = imageNames.stream()
.map(v->{return superiorPath+"/"+v;})
.collect(Collectors.toList());
return imageNames;
}
/**
*
* @param directory
* @param rootDirectory
* @param imageNames
*/
private void traverseDirectory(File directory, File rootDirectory, List<String> imageNames) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
// 递归处理子目录,保持根目录不变
traverseDirectory(file, rootDirectory, imageNames);
imageFiles.addAll(getAllImageFiles(file));
} else if (isImageFile(file)) {
// 添加图片文件(包含相对于根目录的路径)
String relativePath = getRelativePath(file, rootDirectory);
imageNames.add(relativePath);
imageFiles.add(file.getAbsolutePath());
}
}
}
}
/**
*
* @param file
* @param rootDirectory
* @return
*/
private String getRelativePath(File file, File rootDirectory) {
// 使用 Path API 来正确计算相对路径
try {
return rootDirectory.toPath().relativize(file.toPath()).toString();
} catch (IllegalArgumentException e) {
// 如果无法计算相对路径,则返回文件名
return file.getName();
}
}
public static void main(String[] args) {
DatasServiceImpl datasService = new DatasServiceImpl();
List<String> imageNames = datasService.getImageNamesFromPath("D:\\PycharmProjects\\yolo\\runs\\detect\\11");
System.out.println(imageNames);
return imageFiles;
}
/**
*
* @param file
* @param file
* @return
*/
private boolean isImageFile(File file) {
@ -142,22 +106,351 @@ public class DatasServiceImpl extends ServiceImpl<DatasMapper, DatasDO> impleme
fileName.endsWith(".bmp") ||
fileName.endsWith(".webp");
}
/**
*
* @param source
* @param dest
*/
private void copyFile(File source, File dest) throws IOException {
Files.copy(source.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
/**
*
* @param datasDO
* @return Map0=url, 1=, 2=
*/
private List<Map<Integer, String>> processDatasets(DatasDO datasDO) {
List<Map<Integer, String>> imagePaths = new ArrayList<>();
if (datasDO.getDatasets() == null || datasDO.getDatasets().isEmpty()) {
return imagePaths;
}
// 获取字典配置
Map<String, DictDataDO> baseConf = dictDataService.getDictDataList("base_conf");
DictDataDO basePathDO = baseConf.get("base_path");
DictDataDO dataApiPathDO = baseConf.get("data_api_path");
if (basePathDO == null) {
log.error("base_conf.base_path 配置不存在");
return imagePaths;
}
String basePath = basePathDO.getValue();
String dataApiPath = dataApiPathDO != null ? dataApiPathDO.getValue() : "";
// 创建YOLO训练目录
String yoloProjectDir = basePath + File.separator + "yolo" + File.separator + datasDO.getId();
File yoloDir = new File(yoloProjectDir);
if (!yoloDir.exists()) {
yoloDir.mkdirs();
}
// 根据任务类型创建目录结构
String projectType = datasDO.getType();
// 在字典表的visual_type里面
if ("2".equals(projectType)) {
// 分类任务data/train/class1/, data/val/class1/
processClassificationDatasets(datasDO, yoloDir, basePath, dataApiPath, imagePaths);
} else if ("1".equals(projectType)) {
// 检测任务data/images/train/, data/labels/train/
processDetectionDatasets(datasDO, yoloDir, basePath, dataApiPath, imagePaths);
} else {
log.error("未知的任务类型: {}", projectType);
}
return imagePaths;
}
@Resource
TypesService typesService;
/**
*
*
* data/
* train/
* class1/
* class2/
* val/
* class1/
* class2/
*/
private void processClassificationDatasets(DatasDO datasDO, File yoloDir, String basePath,
String dataApiPath, List<Map<Integer, String>> imagePaths) {
// 将所有图片保存到默认路径下,并且创建默认分类
TypesSaveReqVO typesSaveReqVO = new TypesSaveReqVO();
typesSaveReqVO.setDataId(datasDO.getId());
typesSaveReqVO.setName("default");
typesSaveReqVO.setColor("#FF6B6B");
typesService.createTypes(typesSaveReqVO);
// 解析数据集ID
String[] datasetIds = datasDO.getDatasets().split(",");
List<DatasetDO> datasetList = new ArrayList<>();
for (String datasetIdStr : datasetIds) {
try {
Integer datasetId = Integer.parseInt(datasetIdStr.trim());
DatasetDO dataset = datasetService.getDataset(datasetId);
if (dataset != null) {
datasetList.add(dataset);
}
} catch (NumberFormatException e) {
log.warn("无效的数据集ID: {}", datasetIdStr);
}
}
// 收集所有图片
List<String> allImages = new ArrayList<>();
for (DatasetDO dataset : datasetList) {
String datasetPath = dataset.getPath();
if (datasetPath != null && !datasetPath.isEmpty()) {
File datasetDir = new File(datasetPath);
if (datasetDir.exists()) {
allImages.addAll(getAllImageFiles(datasetDir));
}
}
}
// 根据比例分配图片
int totalImages = allImages.size();
int trainCount = (int) (totalImages * datasDO.getTrainRatio() / 100.0);
int valCount = (int) (totalImages * datasDO.getValRatio() / 100.0);
int testCount = totalImages - trainCount - valCount;
Collections.shuffle(allImages);
// 创建目录结构
File dataDir = yoloDir;
File trainDir = new File(dataDir, "train");
File valDir = new File(dataDir, "val");
if (!trainDir.exists()) trainDir.mkdirs();
if (!valDir.exists()) valDir.mkdirs();
// 创建默认类别目录(由于分类任务需要类别,这里先创建一个默认类别)
File defaultClassDir = new File(trainDir, "default");
if (!defaultClassDir.exists()) defaultClassDir.mkdirs();
File defaultClassValDir = new File(valDir, "default");
if (!defaultClassValDir.exists()) defaultClassValDir.mkdirs();
// 处理训练集
for (int i = 0; i < trainCount; i++) {
String imagePath = allImages.get(i);
processClassificationImage(imagePath, defaultClassDir, basePath, dataApiPath,
datasDO.getId(), "train", "default", imagePaths);
}
// 处理验证集
for (int i = trainCount; i < trainCount + valCount; i++) {
String imagePath = allImages.get(i);
processClassificationImage(imagePath, defaultClassValDir, basePath, dataApiPath,
datasDO.getId(), "val", "default", imagePaths);
}
// 测试集(合并到验证集)
for (int i = trainCount + valCount; i < totalImages; i++) {
String imagePath = allImages.get(i);
processClassificationImage(imagePath, defaultClassValDir, basePath, dataApiPath,
datasDO.getId(), "val", "default", imagePaths);
}
}
/**
*
*/
private void processClassificationImage(String imagePath, File destDir, String basePath,
String dataApiPath, Integer projectId, String split,
String className, List<Map<Integer, String>> imagePaths) {
File sourceFile = new File(imagePath);
File destFile = new File(destDir, sourceFile.getName());
try {
copyFile(sourceFile, destFile);
// 生成相对路径
String relativePath = split + "/" + className + "/" + destFile.getName();
// 生成HTTP路径
String httpPath = !dataApiPath.isEmpty()
? dataApiPath + "/yolo/" + projectId + "/" + relativePath
: "/yolo/" + projectId + "/" + relativePath;
// 生成绝对路径
String absolutePath = destFile.getAbsolutePath();
// 构建返回Map0=url路径, 1=文件路径, 2=标注路径(分类没有标注)
Map<Integer, String> result = new java.util.HashMap<>();
result.put(0, httpPath);
result.put(1, absolutePath);
result.put(2, ""); // 分类任务没有标注路径
imagePaths.add(result);
} catch (IOException e) {
log.error("复制文件失败: {}", imagePath, e);
}
}
/**
*
*
* data/
* images/
* train/
* val/
* labels/
* train/
* val/
*/
private void processDetectionDatasets(DatasDO datasDO, File yoloDir, String basePath,
String dataApiPath, List<Map<Integer, String>> imagePaths) {
// 解析数据集ID
String[] datasetIds = datasDO.getDatasets().split(",");
List<DatasetDO> datasetList = new ArrayList<>();
for (String datasetIdStr : datasetIds) {
try {
Integer datasetId = Integer.parseInt(datasetIdStr.trim());
DatasetDO dataset = datasetService.getDataset(datasetId);
if (dataset != null) {
datasetList.add(dataset);
}
} catch (NumberFormatException e) {
log.warn("无效的数据集ID: {}", datasetIdStr);
}
}
// 收集所有图片
List<String> allImages = new ArrayList<>();
for (DatasetDO dataset : datasetList) {
String datasetPath = dataset.getPath();
if (datasetPath != null && !datasetPath.isEmpty()) {
File datasetDir = new File(datasetPath);
if (datasetDir.exists()) {
allImages.addAll(getAllImageFiles(datasetDir));
}
}
}
// 根据比例分配图片
int totalImages = allImages.size();
int trainCount = (int) (totalImages * datasDO.getTrainRatio() / 100.0);
int valCount = (int) (totalImages * datasDO.getValRatio() / 100.0);
int testCount = totalImages - trainCount - valCount;
Collections.shuffle(allImages);
// 创建目录结构
File imagesTrainDir = new File(yoloDir, "images/train");
File imagesValDir = new File(yoloDir, "images/val");
File labelsTrainDir = new File(yoloDir, "labels/train");
File labelsValDir = new File(yoloDir, "labels/val");
if (!imagesTrainDir.exists()) imagesTrainDir.mkdirs();
if (!imagesValDir.exists()) imagesValDir.mkdirs();
if (!labelsTrainDir.exists()) labelsTrainDir.mkdirs();
if (!labelsValDir.exists()) labelsValDir.mkdirs();
// 处理训练集
for (int i = 0; i < trainCount; i++) {
String imagePath = allImages.get(i);
processDetectionImage(imagePath, imagesTrainDir, labelsTrainDir, basePath, dataApiPath,
datasDO.getId(), "train", imagePaths);
}
// 处理验证集
for (int i = trainCount; i < trainCount + valCount; i++) {
String imagePath = allImages.get(i);
processDetectionImage(imagePath, imagesValDir, labelsValDir, basePath, dataApiPath,
datasDO.getId(), "val", imagePaths);
}
// 测试集(合并到验证集)
for (int i = trainCount + valCount; i < totalImages; i++) {
String imagePath = allImages.get(i);
processDetectionImage(imagePath, imagesValDir, labelsValDir, basePath, dataApiPath,
datasDO.getId(), "val", imagePaths);
}
}
/**
*
*/
private void processDetectionImage(String imagePath, File imagesDir, File labelsDir, String basePath,
String dataApiPath, Integer projectId, String split,
List<Map<Integer, String>> imagePaths) {
File sourceFile = new File(imagePath);
File destImageFile = new File(imagesDir, sourceFile.getName());
try {
copyFile(sourceFile, destImageFile);
// 生成标注文件路径
String labelFileName = sourceFile.getName().replaceAll("\\.[^.]+$", ".txt");
File labelFile = new File(labelsDir, labelFileName);
// 创建空的标注文件
if (!labelFile.exists()) {
labelFile.createNewFile();
}
// 生成图片相对路径
String imageRelativePath = "images/" + split + "/" + destImageFile.getName();
// 生成标注文件相对路径
String labelRelativePath = "labels/" + split + "/" + labelFile.getName();
// 生成HTTP路径
String httpPath = !dataApiPath.isEmpty()
? dataApiPath + "/yolo/" + projectId + "/" + imageRelativePath
: "/yolo/" + projectId + "/" + imageRelativePath;
// 生成绝对路径
String absolutePath = destImageFile.getAbsolutePath();
// 生成标注文件绝对路径
String labelAbsolutePath = labelFile.getAbsolutePath();
// 构建返回Map0=url路径, 1=文件路径, 2=标注路径
Map<Integer, String> result = new java.util.HashMap<>();
result.put(0, httpPath);
result.put(1, absolutePath);
result.put(2, labelAbsolutePath);
imagePaths.add(result);
} catch (IOException e) {
log.error("复制文件失败: {}", imagePath, e);
}
}
@Override
public Integer createDatas(DatasSaveReqVO createReqVO) {
// 插入
DatasDO datas = BeanUtils.toBean(createReqVO, DatasDO.class);
// 查看数据集是否存在判断路径是否存在
List<String> imageNames = getImageNamesFromPath(createReqVO.getPath());
//默认类型为正在标注
datas.setStatus("1");
datas.setCount((long) imageNames.size());
// 默认状态为还未标注
datas.setStatus("0");
datas.setProgress(0.0);
for (String imageName : imageNames){
// 先插入数据
datasMapper.insert(datas);
// 处理数据集复制图片并生成YOLO结构
List<Map<Integer, String>> imagePaths = processDatasets(datas);
// 保存图片路径到mark表
for (Map<Integer, String> imageInfo : imagePaths) {
markService.save(new MarkDO()
.setDataId(datas.getId())
.setPath(imageName));
.setPath(imageInfo.get(0)) // 保存URL路径
.setImagePath(imageInfo.get(1)) // 保存图片绝对路径
.setLabelPath(imageInfo.get(2)) // 保存标注文件路径
.setStatus(0)); // 默认未标注
}
datasMapper.insert(datas);
// 更新图片总数
datas.setCount((long) imagePaths.size());
datasMapper.updateById(datas);
// 返回
return datas.getId();
@ -167,8 +460,28 @@ public class DatasServiceImpl extends ServiceImpl<DatasMapper, DatasDO> impleme
public void updateDatas(DatasSaveReqVO updateReqVO) {
// 校验存在
validateDatasExists(updateReqVO.getId());
// 更新
// 删除旧的标记记录
markService.remove(new QueryWrapper<MarkDO>().eq("data_id", updateReqVO.getId()));
// 更新数据
DatasDO updateObj = BeanUtils.toBean(updateReqVO, DatasDO.class);
// 处理数据集复制图片并生成YOLO结构
List<Map<Integer, String>> imagePaths = processDatasets(updateObj);
// 保存图片路径到mark表
for (Map<Integer, String> imageInfo : imagePaths) {
markService.save(new MarkDO()
.setDataId(updateObj.getId())
.setPath(imageInfo.get(0)) // 保存URL路径
.setImagePath(imageInfo.get(1)) // 保存图片绝对路径
.setLabelPath(imageInfo.get(2)) // 保存标注文件路径
.setStatus(0));
}
// 更新图片总数
updateObj.setCount((long) imagePaths.size());
datasMapper.updateById(updateObj);
}
@ -176,10 +489,47 @@ public class DatasServiceImpl extends ServiceImpl<DatasMapper, DatasDO> impleme
public void deleteDatas(Integer id) {
// 校验存在
validateDatasExists(id);
// 删除
// 获取字典配置
Map<String, DictDataDO> baseConf = dictDataService.getDictDataList("base_conf");
DictDataDO basePathDO = baseConf.get("base_path");
if (basePathDO != null) {
String basePath = basePathDO.getValue();
String yoloProjectDir = basePath + File.separator + "yolo" + File.separator + id;
File yoloDir = new File(yoloProjectDir);
// 删除YOLO训练目录
if (yoloDir.exists()) {
deleteDirectory(yoloDir);
}
}
// 删除标记记录
markService.remove(new QueryWrapper<MarkDO>().eq("data_id", id));
// 删除数据
datasMapper.deleteById(id);
}
/**
*
* @param directory
*/
private void deleteDirectory(File directory) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
deleteDirectory(file);
} else {
file.delete();
}
}
}
directory.delete();
}
private void validateDatasExists(Integer id) {
if (datasMapper.selectById(id) == null) {
// throw exception();
@ -195,75 +545,25 @@ public class DatasServiceImpl extends ServiceImpl<DatasMapper, DatasDO> impleme
public PageResult<DatasDO> getDatasPage(DatasPageReqVO pageReqVO) {
return datasMapper.selectPage(pageReqVO);
}
@Override
public void refreshDatas(DatasDO datasDO) {
// 1. 获取路径中的所有图片文件
List<String> currentImageNames = getImageNamesFromPath(datasDO.getPath());
// 2. 获取数据库中已有的标记记录
List<MarkDO> existingMarks = markService.list(new QueryWrapper<MarkDO>().eq("data_id", datasDO.getId()));
// 3. 找出需要删除的图片(数据库中有但文件系统中没有)
List<String> existingImageNames = existingMarks.stream()
.map(MarkDO::getPath)
.collect(Collectors.toList());
List<String> imagesToDelete = existingImageNames.stream()
.filter(imageName -> !currentImageNames.contains(imageName))
.collect(Collectors.toList());
// 删除不存在的标记记录
if (!imagesToDelete.isEmpty()) {
markService.remove(new QueryWrapper<MarkDO>()
.eq("data_id", datasDO.getId())
.in("path", imagesToDelete));
}
// 4. 找出需要新增的图片(文件系统中有但数据库中没有)
List<String> imagesToAdd = currentImageNames.stream()
.filter(imageName -> !existingImageNames.contains(imageName))
.collect(Collectors.toList());
// 新增图片标记记录
for (String imageName : imagesToAdd) {
markService.save(new MarkDO()
.setDataId(datasDO.getId())
.setPath(imageName)
.setStatus(0)); // 默认未标注状态
}
// 5. 更新数据集统计信息
List<MarkDO> updatedMarks = markService.list(new QueryWrapper<MarkDO>().eq("data_id", datasDO.getId()));
// 计算进度
long totalImages = updatedMarks.size();
long completedImages = updatedMarks.stream()
.filter(mark -> mark.getStatus() != null && mark.getStatus() == 1)
.count();
double progress = totalImages > 0 ? (completedImages * 100.0 / totalImages) : 0.0;
// 判断是否全部完成
String status = (completedImages == totalImages && totalImages > 0) ? "2" : "1"; // 2表示完成1表示进行中
// 更新数据集
datasDO.setCount(totalImages);
datasDO.setProgress(progress);
datasDO.setStatus(status);
datasDO.setCreator( null);
datasDO.setUpdater(null);
datasMapper.updateById(datasDO);
// 不再使用path字段此方法需要重新实现
log.warn("refreshDatas方法已废弃请使用updateDatas方法更新数据集");
}
@Resource
ImageService imageService;
@Resource
MarkInfoMapper markInfoMapper;
@Override
public void enhance(DatasEnhance datasEnhance) {
DictDataDO basePathDO = dictDataService.parseDictData("visual_annotation_conf", "base_path");
if (basePathDO == null) {
log.error("visual_annotation_conf.base_path 配置不存在");
return;
}
String basePath = basePathDO.getValue();
DictDataDO basePath = dictDataService.parseDictData("visual_annotation_conf","base_path");
// 遍历图片,根据选择的类型,新增图片,并且新增他自身的标记记录
// 遍历图片,根据选择的类型,新增图片,并且新增他自身的标记记录
List<MarkDO> markDOList = markService.list(new QueryWrapper<MarkDO>()
.eq("data_id", datasEnhance.getId()));
@ -277,7 +577,7 @@ public class DatasServiceImpl extends ServiceImpl<DatasMapper, DatasDO> impleme
try {
String path = imageService.enhanceImage(
markDO.getPath(),
basePath.getValue(),
basePath,
datasEnhance.getEnhancements()[index % datasEnhance.getEnhancements().length]
);
@ -285,20 +585,19 @@ public class DatasServiceImpl extends ServiceImpl<DatasMapper, DatasDO> impleme
new QueryWrapper<MarkInfoDO>().eq("mark_id", markDO.getId())
);
MarkDO newMarkDO = BeanUtils.toBean(markDO, MarkDO.class);
MarkDO newMarkDO = BeanUtils.toBean(markDO, MarkDO.class);
newMarkDO.setId(null);
newMarkDO.setPath(path);
markService.save(newMarkDO);
for (MarkInfoDO markInfoDO : markInfoDOList) {
MarkInfoDO newMarkInfoDO = BeanUtils.toBean(markInfoDO, MarkInfoDO.class);
MarkInfoDO newMarkInfoDO = BeanUtils.toBean(markInfoDO, MarkInfoDO.class);
newMarkInfoDO.setId(null);
newMarkInfoDO.setMarkId(newMarkDO.getId());
markInfoMapper.insert(newMarkInfoDO);
}
} catch (Exception e) {
log.error("图像增强处理失败,图片路径: "+markDO.getPath(),e);
log.error("图像增强处理失败,图片路径: " + markDO.getPath(), e);
}
});
@ -307,8 +606,6 @@ public class DatasServiceImpl extends ServiceImpl<DatasMapper, DatasDO> impleme
// 等待所有异步任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}
}

@ -0,0 +1,65 @@
package cn.iocoder.yudao.module.annotation.service.dataset;
import java.util.*;
import jakarta.validation.*;
import cn.iocoder.yudao.module.annotation.controller.admin.dataset.vo.*;
import cn.iocoder.yudao.module.annotation.dal.dataobject.dataset.DatasetDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.web.multipart.MultipartFile;
/**
* Service
*
* @author
*/
public interface DatasetService {
/**
*
*
* @param createReqVO
* @return
*/
Integer createDataset(@Valid DatasetSaveReqVO createReqVO);
/**
*
*
* @param updateReqVO
*/
void updateDataset(@Valid DatasetSaveReqVO updateReqVO);
/**
*
*
* @param id
*/
void deleteDataset(Integer id);
/**
*
*
* @param id
* @return
*/
DatasetDO getDataset(Integer id);
/**
*
*
* @param pageReqVO
* @return
*/
PageResult<DatasetDO> getDatasetPage(DatasetPageReqVO pageReqVO);
/**
*
*
* @param id ID
* @param files zip
* @return URL
*/
List<String> addImages(Integer id, MultipartFile[] files);
}

@ -0,0 +1,415 @@
package cn.iocoder.yudao.module.annotation.service.dataset;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import cn.iocoder.yudao.module.annotation.controller.admin.dataset.vo.*;
import cn.iocoder.yudao.module.annotation.dal.dataobject.dataset.DatasetDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.annotation.dal.mysql.dataset.DatasetMapper;
import cn.iocoder.yudao.module.system.service.dict.DictDataService;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.annotation.enums.ErrorCodeConstants.*;
import lombok.extern.slf4j.Slf4j;
/**
* Service
*
* @author
*/
@Service
@Validated
@Slf4j
public class DatasetServiceImpl implements DatasetService {
@Resource
private DatasetMapper datasetMapper;
@Resource
private DictDataService dictDataService;
@Override
public Integer createDataset(DatasetSaveReqVO createReqVO) {
// 插入
DatasetDO dataset = BeanUtils.toBean(createReqVO, DatasetDO.class);
// 如果未指定路径,自动生成路径
if (dataset.getPath() == null || dataset.getPath().trim().isEmpty()) {
Map<String, DictDataDO> dictDataMap = dictDataService.getDictDataList("visual_annotation_conf");
String basePath = dictDataMap.get("base_path").getValue();
String baseUrl = dictDataMap.get("base_url").getValue();
// 插入数据库获取ID
datasetMapper.insert(dataset);
Integer id = dataset.getId();
// 生成路径base_path/dataset/id/
String datasetPath = basePath + "dataset/" + id + "/";
String datasetUrl = baseUrl + "dataset/" + id + "/";
// 创建目录
File dir = new File(datasetPath);
if (!dir.exists()) {
dir.mkdirs();
log.info("创建数据集目录: {}", datasetPath);
}
// 更新路径
dataset.setPath(datasetUrl);
datasetMapper.updateById(dataset);
log.info("创建数据集ID={}, 自动生成路径={}", id, datasetUrl);
} else {
// 使用指定路径
datasetMapper.insert(dataset);
}
// 如果有上传文件,处理文件
if (createReqVO.getFiles() != null && createReqVO.getFiles().length > 0) {
processFiles(dataset.getId(), createReqVO.getFiles());
}
// 返回
return dataset.getId();
}
@Override
public void updateDataset(DatasetSaveReqVO updateReqVO) {
// 校验存在
validateDatasetExists(updateReqVO.getId());
// 更新
DatasetDO updateObj = BeanUtils.toBean(updateReqVO, DatasetDO.class);
// 如果更新了路径,且之前路径为空或使用默认路径,需要处理
DatasetDO existingDataset = datasetMapper.selectById(updateReqVO.getId());
if (existingDataset != null && updateObj.getPath() != null &&
!updateObj.getPath().equals(existingDataset.getPath())) {
// 如果指定了新的路径,直接使用
datasetMapper.updateById(updateObj);
} else if (existingDataset != null &&
(existingDataset.getPath() == null || existingDataset.getPath().trim().isEmpty())) {
// 如果之前路径为空,生成默认路径
Map<String, DictDataDO> dictDataMap = dictDataService.getDictDataList("visual_annotation_conf");
String basePath = dictDataMap.get("base_path").getValue();
String baseUrl = dictDataMap.get("base_url").getValue();
String datasetPath = basePath + "dataset/" + updateReqVO.getId() + "/";
String datasetUrl = baseUrl + "dataset/" + updateReqVO.getId() + "/";
// 创建目录
File dir = new File(datasetPath);
if (!dir.exists()) {
dir.mkdirs();
log.info("创建数据集目录: {}", datasetPath);
}
updateObj.setPath(datasetUrl);
datasetMapper.updateById(updateObj);
} else {
datasetMapper.updateById(updateObj);
}
// 如果有上传文件,处理文件
if (updateReqVO.getFiles() != null && updateReqVO.getFiles().length > 0) {
processFiles(updateReqVO.getId(), updateReqVO.getFiles());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteDataset(Integer id) {
// 校验存在
validateDatasetExists(id);
// 获取数据集信息
DatasetDO dataset = datasetMapper.selectById(id);
// 删除数据库记录
datasetMapper.deleteById(id);
// 删除对应的文件夹
if (dataset != null && dataset.getPath() != null && !dataset.getPath().trim().isEmpty()) {
Map<String, DictDataDO> dictDataMap = dictDataService.getDictDataList("visual_annotation_conf");
String basePath = dictDataMap.get("base_path").getValue();
String baseUrl = dictDataMap.get("base_url").getValue();
// 从URL转换为文件路径
String datasetPath = dataset.getPath().replace(baseUrl, basePath);
File datasetDir = new File(datasetPath);
if (datasetDir.exists() && datasetDir.isDirectory()) {
boolean deleted = deleteDirectory(datasetDir);
if (deleted) {
log.info("删除数据集文件夹成功: {}", datasetPath);
} else {
log.warn("删除数据集文件夹失败: {}", datasetPath);
}
}
}
}
@Override
public List<String> addImages(Integer id, MultipartFile[] files) {
// 校验数据集存在
validateDatasetExists(id);
// 获取数据集信息
DatasetDO dataset = datasetMapper.selectById(id);
if (dataset == null || dataset.getPath() == null || dataset.getPath().trim().isEmpty()) {
throw exception(DATASET_PATH_NOT_SET);
}
return processFiles(id, files);
}
/**
* zip
*
* @param datasetId ID
* @param files
* @return URL
*/
private List<String> processFiles(Integer datasetId, MultipartFile[] files) {
if (files == null || files.length == 0) {
log.warn("没有上传任何文件");
return new ArrayList<>();
}
Map<String, DictDataDO> dictDataMap = dictDataService.getDictDataList("visual_annotation_conf");
String basePath = dictDataMap.get("base_path").getValue();
String baseUrl = dictDataMap.get("base_url").getValue();
// 目标文件夹base_path/dataset/id/
File targetDir = new File(basePath + "dataset/" + datasetId + "/");
if (!targetDir.exists()) {
targetDir.mkdirs();
log.info("创建目标文件夹: {}", targetDir.getAbsolutePath());
}
List<String> savedImagePaths = new ArrayList<>();
for (MultipartFile file : files) {
try {
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || originalFilename.trim().isEmpty()) {
log.warn("文件名为空,跳过");
continue;
}
// 检查是否是zip文件
if (originalFilename.toLowerCase().endsWith(".zip")) {
// 解压zip文件
List<String> extractedFiles = extractZipFile(file, targetDir, baseUrl, datasetId);
savedImagePaths.addAll(extractedFiles);
log.info("解压zip文件: {}, 提取 {} 个文件", originalFilename, extractedFiles.size());
} else if (isImageFile(originalFilename)) {
// 保存图片文件
String savedPath = saveImageFile(file, targetDir, baseUrl, datasetId);
if (savedPath != null && !savedPath.isEmpty()) {
savedImagePaths.add(savedPath);
}
} else {
log.warn("非zip或图片文件跳过: {}", originalFilename);
}
} catch (IOException e) {
log.error("处理文件失败: {}", file.getOriginalFilename(), e);
}
}
log.info("处理文件完成数据集ID={}, 共保存 {} 个文件", datasetId, savedImagePaths.size());
return savedImagePaths;
}
/**
* zip
*
* @param zipFile zip
* @param targetDir
* @param baseUrl URL
* @param datasetId ID
* @return URL
*/
private List<String> extractZipFile(MultipartFile zipFile, File targetDir, String baseUrl, Integer datasetId) throws IOException {
List<String> extractedFiles = new ArrayList<>();
try (ZipInputStream zipIn = new ZipInputStream(zipFile.getInputStream())) {
ZipEntry entry;
byte[] buffer = new byte[1024];
while ((entry = zipIn.getNextEntry()) != null) {
String entryName = entry.getName();
// 跳过目录
if (entry.isDirectory()) {
continue;
}
// 检查是否是图片文件
if (!isImageFile(entryName)) {
log.debug("跳过非图片文件: {}", entryName);
continue;
}
// 提取文件名(去掉路径)
String fileName = getFileNameFromPath(entryName);
File outputFile = new File(targetDir, fileName);
// 保存文件
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
int len;
while ((len = zipIn.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
}
String fileUrl = baseUrl + "dataset/" + datasetId + "/" + fileName;
extractedFiles.add(fileUrl);
log.info("解压文件: {} -> {}", entryName, fileUrl);
}
zipIn.closeEntry();
}
return extractedFiles;
}
/**
*
*
* @param file
* @param targetDir
* @param baseUrl URL
* @param datasetId ID
* @return URL
*/
private String saveImageFile(MultipartFile file, File targetDir, String baseUrl, Integer datasetId) throws IOException {
String originalFilename = file.getOriginalFilename();
String fileExtension = getFileExtension(originalFilename);
// 生成唯一文件名(避免重复)
String uniqueFileName = generateUniqueFileName(targetDir, originalFilename, fileExtension);
File targetFile = new File(targetDir, uniqueFileName);
// 保存文件
file.transferTo(targetFile);
log.info("保存图片: {} -> {}", originalFilename, targetFile.getAbsolutePath());
// 返回URL路径
return baseUrl + "dataset/" + datasetId + "/" + uniqueFileName;
}
/**
*
*
* @param directory
* @return
*/
private boolean deleteDirectory(File directory) {
if (directory == null || !directory.exists()) {
return false;
}
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
deleteDirectory(file);
} else {
file.delete();
}
}
}
return directory.delete();
}
/**
*
*/
private boolean isImageFile(String fileName) {
String lowerName = fileName.toLowerCase();
return lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg") ||
lowerName.endsWith(".png") || lowerName.endsWith(".bmp") ||
lowerName.endsWith(".gif");
}
/**
*
*/
private String getFileExtension(String fileName) {
int lastDotIndex = fileName.lastIndexOf(".");
if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) {
return fileName.substring(lastDotIndex + 1).toLowerCase();
}
return "";
}
/**
*
*/
private String getFileNameFromPath(String path) {
int lastSlashIndex = path.lastIndexOf("/");
if (lastSlashIndex >= 0) {
return path.substring(lastSlashIndex + 1);
}
return path;
}
/**
*
*/
private String generateUniqueFileName(File targetDir, String originalFileName, String extension) {
String baseName = originalFileName;
int dotIndex = originalFileName.lastIndexOf(".");
if (dotIndex > 0) {
baseName = originalFileName.substring(0, dotIndex);
}
String fileName = originalFileName;
int counter = 1;
// 如果文件已存在,添加序号
File file = new File(targetDir, fileName);
while (file.exists()) {
fileName = baseName + "_" + counter + "." + extension;
file = new File(targetDir, fileName);
counter++;
}
return fileName;
}
private void validateDatasetExists(Integer id) {
if (datasetMapper.selectById(id) == null) {
throw exception(DATASET_NOT_EXISTS);
}
}
@Override
public DatasetDO getDataset(Integer id) {
return datasetMapper.selectById(id);
}
@Override
public PageResult<DatasetDO> getDatasetPage(DatasetPageReqVO pageReqVO) {
return datasetMapper.selectPage(pageReqVO);
}
}

@ -52,4 +52,33 @@ public interface MarkService extends IService<MarkDO> {
*/
PageResult<MarkDO> getMarkPage(MarkPageReqVO pageReqVO);
/**
*
*
* @param markId ID
*/
void generateAnnotationFile(Integer markId);
/**
*
*
* @param markId ID
*/
void updateAnnotationFile(Integer markId);
/**
*
*
* @param markId ID
*/
void deleteAnnotationFile(Integer markId);
/**
*
*
* @param markId ID
* @return
*/
String readAnnotationFile(Integer markId);
}

@ -1,15 +1,41 @@
package cn.iocoder.yudao.module.annotation.service.mark;
import cn.hutool.core.io.FileUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.annotation.controller.admin.mark.vo.MarkPageReqVO;
import cn.iocoder.yudao.module.annotation.controller.admin.mark.vo.MarkSaveReqVO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.datas.DatasDO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.mark.MarkDO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.mark.MarkInfoDO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.types.TypesDO;
import cn.iocoder.yudao.module.annotation.dal.mysql.mark.MarkMapper;
import cn.iocoder.yudao.module.annotation.service.MarkInfo.AnnotationData;
import cn.iocoder.yudao.module.annotation.service.MarkInfo.MarkInfoService;
import cn.iocoder.yudao.module.annotation.service.datas.DatasService;
import cn.iocoder.yudao.module.annotation.service.types.TypesService;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
import cn.iocoder.yudao.module.system.service.dict.DictDataService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
//import static cn.iocoder.yudao.module.annotation.enums.ErrorCodeConstants.*;
/**
@ -17,6 +43,7 @@ import org.springframework.validation.annotation.Validated;
*
* @author
*/
@Slf4j
@Service
@Validated
public class MarkServiceImpl extends ServiceImpl<MarkMapper, MarkDO> implements MarkService {
@ -24,6 +51,19 @@ public class MarkServiceImpl extends ServiceImpl<MarkMapper, MarkDO> implements
@Resource
private MarkMapper markMapper;
@Resource
private MarkInfoService markInfoService;
@Lazy
@Resource
private DatasService datasService;
@Resource
private TypesService typesService;
@Resource
private DictDataService dictDataService;
@Override
public Integer createMark(MarkSaveReqVO createReqVO) {
// 插入
@ -47,7 +87,8 @@ public class MarkServiceImpl extends ServiceImpl<MarkMapper, MarkDO> implements
public void deleteMark(Integer id) {
// 校验存在
validateMarkExists(id);
// 删除
// 删除数据库记录
markMapper.deleteById(id);
}
@ -67,4 +108,367 @@ public class MarkServiceImpl extends ServiceImpl<MarkMapper, MarkDO> implements
return markMapper.selectPage(pageReqVO);
}
@Override
public void generateAnnotationFile(Integer markId) {
MarkDO mark = markMapper.selectById(markId);
if (mark == null) {
log.error("标注不存在: {}", markId);
return;
}
DatasDO datas = datasService.getDatas(mark.getDataId());
if (datas == null) {
log.error("项目不存在: {}", mark.getDataId());
return;
}
String projectType = datas.getType();
String projectPath = datas.getPath();
try {
if ("1".equals(projectType)) {
// 检测任务生成YOLO格式的标注文件
generateDetectionAnnotationFile(mark, projectPath);
} else if ("2".equals(projectType)) {
// 分类任务:移动图片到对应的类别目录
generateClassificationAnnotation(mark, projectPath, datas);
}
} catch (Exception e) {
log.error("生成标注文件失败: markId={}, projectType={}", markId, projectType, e);
}
}
@Override
public void updateAnnotationFile(Integer markId) {
MarkDO mark = markMapper.selectById(markId);
if (mark == null) {
log.error("标注不存在: {}", markId);
return;
}
DatasDO datas = datasService.getDatas(mark.getDataId());
if (datas == null) {
log.error("项目不存在: {}", mark.getDataId());
return;
}
String projectType = datas.getType();
String projectPath = datas.getPath();
try {
if ("1".equals(projectType)) {
// 检测任务更新YOLO格式的标注文件
updateDetectionAnnotationFile(mark, projectPath);
} else if ("2".equals(projectType)) {
// 分类任务:更新图片的类别位置
updateClassificationAnnotation(mark, projectPath, datas);
}
} catch (Exception e) {
log.error("更新标注文件失败: markId={}, projectType={}", markId, projectType, e);
}
}
@Override
public void deleteAnnotationFile(Integer markId) {
MarkDO mark = markMapper.selectById(markId);
if (mark == null) {
log.error("标注不存在: {}", markId);
return;
}
DatasDO datas = datasService.getDatas(mark.getDataId());
if (datas == null) {
log.error("项目不存在: {}", mark.getDataId());
return;
}
String projectType = datas.getType();
String projectPath = datas.getPath();
try {
if ("1".equals(projectType)) {
// 检测任务:删除标注文件
deleteDetectionAnnotationFile(mark, projectPath);
} else if ("2".equals(projectType)) {
// 分类任务:删除图片文件
deleteClassificationAnnotation(mark, projectPath);
}
} catch (Exception e) {
log.error("删除标注文件失败: markId={}, projectType={}", markId, projectType, e);
}
}
@Override
public String readAnnotationFile(Integer markId) {
MarkDO mark = markMapper.selectById(markId);
if (mark == null) {
log.error("标注不存在: {}", markId);
return null;
}
DatasDO datas = datasService.getDatas(mark.getDataId());
if (datas == null) {
log.error("项目不存在: {}", mark.getDataId());
return null;
}
String projectType = datas.getType();
String projectPath = datas.getPath();
try {
if ("1".equals(projectType)) {
// 检测任务:读取标注文件内容
return readDetectionAnnotationFile(mark, projectPath);
} else if ("2".equals(projectType)) {
// 分类任务:返回类别信息
return readClassificationAnnotation(mark, datas);
}
} catch (Exception e) {
log.error("读取标注文件失败: markId={}, projectType={}", markId, projectType, e);
}
return null;
}
/**
* YOLO
*/
private void generateDetectionAnnotationFile(MarkDO mark, String projectPath) throws Exception {
// 获取基础路径
DictDataDO basePathDO = dictDataService.parseDictData("visual_annotation_conf", "base_path");
String basePath = basePathDO != null ? basePathDO.getValue() : "";
// 获取图片路径
String imagePath = basePath + mark.getPath();
File imageFile = new File(imagePath);
if (!imageFile.exists()) {
log.error("图片文件不存在: {}", imagePath);
return;
}
// 获取图片尺寸
BufferedImage image = ImageIO.read(imageFile);
int imageWidth = image.getWidth();
int imageHeight = image.getHeight();
// 获取项目类型映射
List<TypesDO> typesList = typesService.list(new QueryWrapper<TypesDO>().eq("data_id", mark.getDataId()));
typesList.sort(Comparator.comparing(TypesDO::getId));
Map<Integer, Integer> classIdMap = new HashMap<>();
for (int i = 0; i < typesList.size(); i++) {
classIdMap.put(typesList.get(i).getId(), i);
}
// 获取标注信息
List<MarkInfoDO> markInfoList = markInfoService.list(new QueryWrapper<MarkInfoDO>()
.eq("data_id", mark.getDataId()).eq("mark_id", mark.getId()));
// 确定标注文件目录labels目录下
String labelFileName = imageFile.getName().replaceAll("\\.[^.]+$", ".txt");
File labelFile = new File(projectPath, "labels/" + labelFileName);
File labelDir = labelFile.getParentFile();
if (!labelDir.exists()) {
labelDir.mkdirs();
}
// 生成YOLO格式的标注文件
try (PrintWriter writer = new PrintWriter(new FileWriter(labelFile))) {
for (MarkInfoDO markInfo : markInfoList) {
Integer classIndex = classIdMap.get(markInfo.getClassId());
if (classIndex != null) {
AnnotationData annotationData = parseAnnotationData(markInfo);
if (annotationData != null) {
String yoloLine = convertToYOLOFormat(annotationData, classIndex, imageWidth, imageHeight);
if (yoloLine != null) {
writer.println(yoloLine);
}
}
}
}
}
// 更新数据库中的标注文件路径
mark.setPath("labels/" + labelFileName);
markMapper.updateById(mark);
log.info("生成检测标注文件成功: {}", labelFile.getAbsolutePath());
}
/**
*
*/
private void generateClassificationAnnotation(MarkDO mark, String projectPath, DatasDO datas) throws Exception {
// 获取基础路径
DictDataDO basePathDO = dictDataService.parseDictData("visual_annotation_conf", "base_path");
String basePath = basePathDO != null ? basePathDO.getValue() : "";
// 获取标注信息(分类通常只有一个主要类别)
List<MarkInfoDO> markInfoList = markInfoService.list(new QueryWrapper<MarkInfoDO>()
.eq("data_id", mark.getDataId()).eq("mark_id", mark.getId()));
if (markInfoList.isEmpty()) {
log.error("没有找到标注信息: markId={}", mark.getId());
return;
}
// 获取类别名称
TypesDO type = typesService.getById(markInfoList.get(0).getClassId());
if (type == null) {
log.error("类别不存在: classId={}", markInfoList.get(0).getClassId());
return;
}
String className = type.getName();
// 源图片路径
String sourceImagePath = basePath + mark.getPath();
File sourceImage = new File(sourceImagePath);
if (!sourceImage.exists()) {
log.error("源图片文件不存在: {}", sourceImagePath);
return;
}
// 确定目标目录images/train/classify/类名/
String targetDirPath = projectPath + "/images/train/classify/" + className;
File targetDir = new File(targetDirPath);
if (!targetDir.exists()) {
targetDir.mkdirs();
}
// 移动图片到目标目录
String imageFileName = sourceImage.getName();
File targetImage = new File(targetDir, imageFileName);
Files.move(sourceImage.toPath(), targetImage.toPath(), StandardCopyOption.REPLACE_EXISTING);
// 更新数据库中的图片路径
String relativePath = targetDirPath.substring(projectPath.length() + 1) + "/" + imageFileName;
mark.setPath(relativePath);
markMapper.updateById(mark);
log.info("生成分类标注成功: {}", targetImage.getAbsolutePath());
}
/**
*
*/
private void updateDetectionAnnotationFile(MarkDO mark, String projectPath) throws Exception {
// 重新生成标注文件
generateDetectionAnnotationFile(mark, projectPath);
}
/**
*
*/
private void updateClassificationAnnotation(MarkDO mark, String projectPath, DatasDO datas) throws Exception {
// 重新移动图片到新的类别目录
generateClassificationAnnotation(mark, projectPath, datas);
}
/**
*
*/
private void deleteDetectionAnnotationFile(MarkDO mark, String projectPath) throws IOException {
File labelFile = new File(projectPath, mark.getPath());
if (labelFile.exists()) {
FileUtil.del(labelFile);
log.info("删除检测标注文件成功: {}", labelFile.getAbsolutePath());
}
}
/**
*
*/
private void deleteClassificationAnnotation(MarkDO mark, String projectPath) throws IOException {
// 获取基础路径
DictDataDO basePathDO = dictDataService.parseDictData("visual_annotation_conf", "base_path");
String basePath = basePathDO != null ? basePathDO.getValue() : "";
File imageFile = new File(basePath, mark.getPath());
if (imageFile.exists()) {
FileUtil.del(imageFile);
log.info("删除分类图片成功: {}", imageFile.getAbsolutePath());
}
}
/**
*
*/
private String readDetectionAnnotationFile(MarkDO mark, String projectPath) throws IOException {
File labelFile = new File(projectPath, mark.getPath());
if (!labelFile.exists()) {
return "";
}
return FileUtil.readUtf8String(labelFile);
}
/**
*
*/
private String readClassificationAnnotation(MarkDO mark, DatasDO datas) {
// 获取标注信息
List<MarkInfoDO> markInfoList = markInfoService.list(new QueryWrapper<MarkInfoDO>()
.eq("data_id", mark.getDataId()).eq("mark_id", mark.getId()));
if (!markInfoList.isEmpty()) {
TypesDO type = typesService.getById(markInfoList.get(0).getClassId());
if (type != null) {
return type.getName();
}
}
return "";
}
/**
*
*/
private AnnotationData parseAnnotationData(MarkInfoDO markInfo) {
if (markInfo.getAnnotationData() != null) {
return markInfo.getAnnotationData();
}
if (markInfo.getAnnotationDataString() != null && !markInfo.getAnnotationDataString().isEmpty()) {
return AnnotationData.fromString(markInfo.getAnnotationDataString());
}
return null;
}
/**
* YOLO
*/
private String convertToYOLOFormat(AnnotationData annotationData, Integer classIndex,
int imageWidth, int imageHeight) {
try {
AnnotationData.Target.Selector.Geometry geometry = annotationData.getTarget().getSelector().getGeometry();
// 获取边界框信息
double x = geometry.getX() != null ? geometry.getX() : 0;
double y = geometry.getY() != null ? geometry.getY() : 0;
double w = geometry.getW() != null ? geometry.getW() : 0;
double h = geometry.getH() != null ? geometry.getH() : 0;
// 如果使用基本字段
if (x == 0 && y == 0 && w == 0 && h == 0) {
return null;
}
// 转换为YOLO格式相对坐标和宽高
double centerX = (x + w / 2) / imageWidth;
double centerY = (y + h / 2) / imageHeight;
double width = w / imageWidth;
double height = h / imageHeight;
// 确保坐标在0-1范围内
centerX = Math.max(0, Math.min(1, centerX));
centerY = Math.max(0, Math.min(1, centerY));
width = Math.max(0, Math.min(1, width));
height = Math.max(0, Math.min(1, height));
return String.format("%d %.6f %.6f %.6f %.6f", classIndex, centerX, centerY, width, height);
} catch (Exception e) {
log.error("转换YOLO格式失败", e);
return null;
}
}
}

@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.annotation.service.model;
import java.util.*;
import jakarta.validation.*;
import cn.iocoder.yudao.module.annotation.controller.admin.model.vo.*;
import cn.iocoder.yudao.module.annotation.dal.dataobject.model.ModelDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
/**
* Service
*
* @author
*/
public interface ModelService {
/**
*
*
* @param createReqVO
* @return
*/
Integer createModel(@Valid ModelSaveReqVO createReqVO);
/**
*
*
* @param updateReqVO
*/
void updateModel(@Valid ModelSaveReqVO updateReqVO);
/**
*
*
* @param id
*/
void deleteModel(Integer id);
/**
*
*
* @param id
* @return
*/
ModelDO getModel(Integer id);
/**
*
*
* @param pageReqVO
* @return
*/
PageResult<ModelDO> getModelPage(ModelPageReqVO pageReqVO);
}

@ -0,0 +1,74 @@
package cn.iocoder.yudao.module.annotation.service.model;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import cn.iocoder.yudao.module.annotation.controller.admin.model.vo.*;
import cn.iocoder.yudao.module.annotation.dal.dataobject.model.ModelDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.annotation.dal.mysql.model.ModelMapper;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.annotation.enums.ErrorCodeConstants.*;
/**
* Service
*
* @author
*/
@Service
@Validated
public class ModelServiceImpl implements ModelService {
@Resource
private ModelMapper modelMapper;
@Override
public Integer createModel(ModelSaveReqVO createReqVO) {
// 插入
ModelDO model = BeanUtils.toBean(createReqVO, ModelDO.class);
modelMapper.insert(model);
// 返回
return model.getId();
}
@Override
public void updateModel(ModelSaveReqVO updateReqVO) {
// 校验存在
validateModelExists(updateReqVO.getId());
// 更新
ModelDO updateObj = BeanUtils.toBean(updateReqVO, ModelDO.class);
modelMapper.updateById(updateObj);
}
@Override
public void deleteModel(Integer id) {
// 校验存在
validateModelExists(id);
// 删除
modelMapper.deleteById(id);
}
private void validateModelExists(Integer id) {
if (modelMapper.selectById(id) == null) {
throw exception(MODEL_NOT_EXISTS);
}
}
@Override
public ModelDO getModel(Integer id) {
return modelMapper.selectById(id);
}
@Override
public PageResult<ModelDO> getModelPage(ModelPageReqVO pageReqVO) {
return modelMapper.selectPage(pageReqVO);
}
}

@ -1,12 +1,14 @@
package cn.iocoder.yudao.module.annotation.service.python;
import cn.iocoder.yudao.module.annotation.service.python.vo.PythonVirtualEnvInfo;
import cn.iocoder.yudao.module.annotation.service.yolo.YoloTrainConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* Python
@ -17,6 +19,147 @@ import java.io.InputStreamReader;
@Service
public class PythonVirtualEnvServiceImpl implements PythonVirtualEnvService {
/**
* YOLO 2026/4/10
* @param config
* @return
*/
public static String executeTraining(YoloTrainConfig config) {
// ========== 配置参数 ==========
// YOLO训练参数
String modelPath = config.getModelPath();
String datasetPath = config.getDatasetPath();
int epochs = config.getEpochs();
int batchSize = config.getBatchSize();
int imageSize = config.getImageSize();
String device = config.getDevice();
String outputPath = config.getOutputPath();
String logFilePath = config.getLogFilePath();
int projectId = config.getProjectId();
String venvPath = config.getVenvPath();
String resultPath = null;
// 检测操作系统
boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows");
// 虚拟环境激活脚本路径
String activateScript;
String shellCommand;
if (isWindows) {
activateScript = venvPath + "/Scripts/activate.bat";
shellCommand = "cmd";
} else {
activateScript = venvPath + "/bin/activate";
shellCommand = "bash";
}
String batchFilePath = outputPath + "/train" + projectId + (isWindows ? ".bat" : ".sh");
// 创建批处理文件内容
StringBuilder batchContent = new StringBuilder();
if (isWindows) {
// Windows 批处理文件
batchContent.append("@echo off\r\n");
batchContent.append("call \"").append(activateScript).append("\"\r\n");
batchContent.append("yolo train model=\"").append(modelPath).append("\" data=\"").append(datasetPath)
.append("\" epochs=").append(epochs).append(" batch=").append(batchSize)
.append(" imgsz=").append(imageSize).append(" device=").append(device)
.append(" project=\"").append(outputPath).append("\" name=train").append(projectId).append("\"\r\n");
} else {
// Linux Shell 脚本
batchContent.append("#!/bin/bash\n");
batchContent.append("source \"").append(activateScript).append("\"\n");
batchContent.append("yolo train model=\"").append(modelPath).append("\" data=\"").append(datasetPath)
.append("\" epochs=").append(epochs).append(" batch=").append(batchSize)
.append(" imgsz=").append(imageSize).append(" device=").append(device)
.append(" project=\"").append(outputPath).append("\" name=train").append(projectId).append("\"\n");
}
System.out.println("========================================");
System.out.println("YOLO训练开始");
System.out.println("========================================");
System.out.println("模型: " + modelPath);
System.out.println("数据集: " + datasetPath);
System.out.println("训练轮数: " + epochs);
System.out.println("批次大小: " + batchSize);
System.out.println("图像大小: " + imageSize);
System.out.println("设备: " + device);
System.out.println("输出路径: " + outputPath);
System.out.println("日志文件: " + logFilePath);
System.out.println("项目编号: " + projectId);
System.out.println("虚拟环境路径: " + venvPath);
System.out.println("操作系统: " + (isWindows ? "Windows" : "Linux"));
System.out.println("========================================");
try {
// 创建输出目录
Files.createDirectories(Paths.get(outputPath));
// 写入脚本文件
Files.write(Paths.get(batchFilePath), batchContent.toString().getBytes(StandardCharsets.UTF_8));
System.out.println("脚本文件已创建: " + batchFilePath);
// 如果是Linux需要给脚本添加执行权限
if (!isWindows) {
new File(batchFilePath).setExecutable(true);
}
// 执行脚本文件
ProcessBuilder processBuilder;
if (isWindows) {
processBuilder = new ProcessBuilder("cmd", "/c", batchFilePath);
} else {
processBuilder = new ProcessBuilder(shellCommand, batchFilePath);
}
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
// 读取输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
BufferedWriter logWriter = new BufferedWriter(new FileWriter(logFilePath, StandardCharsets.UTF_8));
String line;
while ((line = reader.readLine()) != null) {
// 输出到控制台
System.out.println(line);
// 解析训练结果路径
if (line.contains("Results saved to ")) {
resultPath = line.substring(line.indexOf("Results saved to ") + 16).trim();
}
// 写入日志文件
logWriter.write(line);
logWriter.newLine();
}
logWriter.close();
reader.close();
// 等待进程结束
int exitCode = process.waitFor();
System.out.println("========================================");
if (exitCode == 0) {
System.out.println("训练完成!");
} else {
System.out.println("训练异常结束,退出码: " + exitCode);
}
System.out.println("日志已保存到: " + logFilePath);
System.out.println("训练结果路径: " + (resultPath != null ? resultPath : outputPath));
System.out.println("========================================");
} catch (Exception e) {
System.err.println("训练过程中发生错误: " + e.getMessage());
e.printStackTrace();
}
return resultPath;
}
@Override
public PythonVirtualEnvInfo activateVirtualEnv(String pythonPath, String envName) {
PythonVirtualEnvInfo info = new PythonVirtualEnvInfo();

@ -0,0 +1,267 @@
# 如何在Spring环境中使用YOLO训练
## 问题说明
如果你直接 `new YoloOperationServiceImpl()`,会遇到 `NullPointerException` 错误,因为所有通过 `@Resource` 注解注入的依赖都是 `null`
## 解决方案
### 方式1运行 YoloTrainMain.java推荐
`YoloTrainMain.java` 已经修改为自动启动Spring环境并正确注入依赖。
**步骤:**
1. 打开 `YoloTrainMain.java`
2. 确保主启动类路径正确第32行
3. 修改配置参数Python路径、虚拟环境、数据集等
4. 直接运行 `main` 方法
**代码示例:**
```java
public static void main(String[] args) {
// 启动Spring应用上下文
ConfigurableApplicationContext context = new SpringApplicationBuilder(
cn.iocoder.yudao.server.YudaoServerApplication.class) // 确保这个路径正确
.headless(false)
.run(args);
// 从Spring容器获取YoloOperationService
YoloOperationService yoloService = context.getBean(YoloOperationService.class);
// 配置参数...
YoloConfig config = new YoloConfig();
config.setTaskType("detect");
config.setModelPath("D:/models/model.pt");
config.setDatasetPath("D:/datasets/data.yaml");
// ... 其他配置
// 启动训练
CompletableFuture<TrainResult> future =
yoloService.trainAsyncWithConfig("python", "yolo", config, "/path/to/log.txt");
// 等待完成
future.join();
// 关闭Spring上下文
context.close();
}
```
### 方式2在Spring Service中使用推荐用于生产环境
如果你的代码已经在Spring环境中运行直接注入即可。
**代码示例:**
```java
@Service
public class MyTrainingService {
@Resource
private YoloOperationService yoloOperationService;
public void startTraining() {
YoloConfig config = new YoloConfig();
config.setTaskType("detect");
config.setModelPath("D:/models/model.pt");
config.setDatasetPath("D:/datasets/data.yaml");
config.setEpochs(100);
config.setBatchSize(16);
config.setOutputPath("D:/yolo_output");
String logFilePath = "D:/yolo_output/training_log.txt";
CompletableFuture<TrainResult> future =
yoloOperationService.trainAsyncWithConfig("python", "yolo", config, logFilePath);
// 异步处理结果
future.thenAccept(result -> {
if ("completed".equals(result.getStatus())) {
log.info("训练成功完成");
log.info("导出路径: {}", result.getExportPath());
} else {
log.error("训练失败: {}", result.getErrorMessage());
}
});
}
}
```
### 方式3在Controller中使用Web应用
如果通过Web API调用训练功能
```java
@RestController
@RequestMapping("/api/yolo")
public class YoloController {
@Resource
private YoloOperationService yoloOperationService;
@PostMapping("/train")
public CommonResult<String> startTraining(@RequestBody YoloConfig config) {
try {
// 设置默认参数
if (config.getEpochs() == 0) {
config.setEpochs(100);
}
if (config.getBatchSize() == 0) {
config.setBatchSize(16);
}
String logFilePath = config.getOutputPath() + "/training_log.txt";
// 启动异步训练
CompletableFuture<TrainResult> future =
yoloOperationService.trainAsyncWithConfig(
"python", "yolo", config, logFilePath);
// 返回任务ID
return success(future.get().getTaskId());
} catch (Exception e) {
return fail("训练启动失败: " + e.getMessage());
}
}
}
```
## 常见问题
### Q1: 启动时找不到 YudaoServerApplication 类?
**A:** 修改 `YoloTrainMain.java` 第32行改为你的主启动类路径
```java
// 修改为你的主启动类
ConfigurableApplicationContext context = new SpringApplicationBuilder(
com.your.package.YourApplication.class) // 修改这里
.headless(false)
.run(args);
```
### Q2: 如何查找主启动类?
**A:** 主启动类通常有 `@SpringBootApplication` 注解,例如:
```java
@SpringBootApplication
public class YudaoServerApplication {
public static void main(String[] args) {
SpringApplication.run(YudaoServerApplication.class, args);
}
}
```
### Q3: 可以不启动Spring环境吗
**A:** 不推荐。`YoloOperationServiceImpl` 依赖多个服务PythonVirtualEnvService, TrainService等这些服务都需要Spring容器管理。
### Q4: 训练任务会阻塞主线程吗?
**A:** 不会。`trainAsyncWithConfig` 返回 `CompletableFuture`,训练在异步线程中执行。
### Q5: 如何查看训练进度?
**A:** 可以通过以下方式:
1. 查看日志文件 `training_log.txt`
2. 使用 `TrainResult` 对象的 `progress` 字段(需要通过轮询或回调)
3. 添加一个查询接口,返回训练状态
## 完整示例Spring Controller + 训练进度查询
```java
@RestController
@RequestMapping("/api/yolo")
public class YoloTrainingController {
@Resource
private YoloOperationService yoloOperationService;
// 存储训练任务生产环境建议使用数据库或Redis
private static final Map<String, TrainResult> trainingTasks = new ConcurrentHashMap<>();
/**
* 启动训练
*/
@PostMapping("/train/start")
public CommonResult<String> startTraining(@RequestBody YoloConfig config) {
try {
// 参数校验
if (config.getDatasetPath() == null || config.getDatasetPath().isEmpty()) {
return fail("数据集路径不能为空");
}
// 设置日志文件路径
String logFilePath = config.getOutputPath() + "/training_log_" +
System.currentTimeMillis() + ".txt";
// 启动异步训练
CompletableFuture<TrainResult> future =
yoloOperationService.trainAsyncWithConfig(
"python", "yolo", config, logFilePath);
// 异步处理结果
future.thenAccept(result -> {
String taskId = result.getTaskId();
trainingTasks.put(taskId, result);
if ("completed".equals(result.getStatus())) {
log.info("训练完成: taskId={}, exportPath={}",
taskId, result.getExportPath());
} else if ("failed".equals(result.getStatus())) {
log.error("训练失败: taskId={}, error={}",
taskId, result.getErrorMessage());
}
});
// 返回任务ID先使用临时ID实际应该从future中获取
String taskId = UUID.randomUUID().toString();
return success(taskId);
} catch (Exception e) {
return fail("训练启动失败: " + e.getMessage());
}
}
/**
* 查询训练进度
*/
@GetMapping("/train/progress/{taskId}")
public CommonResult<TrainResult> getProgress(@PathVariable String taskId) {
TrainResult result = trainingTasks.get(taskId);
if (result == null) {
return fail("任务不存在");
}
return success(result);
}
/**
* 获取训练日志
*/
@GetMapping("/train/log/{taskId}")
public CommonResult<String> getLog(@PathVariable String taskId) {
TrainResult result = trainingTasks.get(taskId);
if (result == null) {
return fail("任务不存在");
}
try {
String logFilePath = "D:/yolo_output/training_log_" + taskId + ".txt";
String logContent = Files.readString(Path.of(logFilePath));
return success(logContent);
} catch (Exception e) {
return fail("读取日志失败: " + e.getMessage());
}
}
}
```
## 总结
**推荐方式**:使用 `YoloTrainMain.java`它已经配置好Spring环境
**生产环境**在你的Service或Controller中注入 `YoloOperationService`
**不要**:直接 `new YoloOperationServiceImpl()`
如有问题,请检查:
1. 主启动类路径是否正确
2. Spring配置是否正确
3. 依赖的服务是否都正常启动

@ -0,0 +1,312 @@
# YOLO训练使用说明
## 概述
新的YOLO训练方法 `trainAsyncWithConfig` 提供了更灵活的配置选项,支持检测和分类任务,并改进了日志处理。
## 主要特性
1. **支持多种任务类型**
- 目标检测 (detect)
- 图像分类 (classify)
- 实例分割 (segment)
2. **灵活的模型配置**
- 使用预训练模型 (yolov8n, yolov8s, etc.)
- 使用自定义模型文件 (.pt 或 .yaml)
3. **完整的训练参数**
- epochs, batch, imgsz
- learningRate, device, optimizer
- workers, savePeriod
4. **改进的日志处理**
- 自动过滤进度条信息
- 保留每轮批次信息
- 写入到txt文件
5. **导出路径记录**
- 自动保存模型导出文件夹路径
## 使用方法
### 方法一:直接运行 Main 函数
1. 打开 `YoloTrainMain.java`
2. 修改配置参数
3. 运行 `main` 方法
```java
public static void main(String[] args) {
// 配置参数
String pythonPath = "python";
String envName = "yolo";
YoloConfig config = new YoloConfig();
config.setTaskType("detect");
config.setModelName("yolov8n");
config.setDatasetPath("D:/datasets/dataset.yaml");
config.setEpochs(100);
config.setBatchSize(16);
config.setImageSize(640);
config.setOutputPath("D:/yolo_output");
String logFilePath = "D:/yolo_output/training_log.txt";
// 执行训练
YoloOperationServiceImpl service = new YoloOperationServiceImpl();
CompletableFuture<TrainResult> future =
service.trainAsyncWithConfig(pythonPath, envName, config, logFilePath);
// 等待结果
future.thenAccept(result -> {
System.out.println("训练完成: " + result.getStatus());
System.out.println("导出路径: " + result.getExportPath());
});
future.join();
}
```
### 方法二在Spring中使用
```java
@Service
public class MyTrainingService {
@Resource
private YoloOperationService yoloOperationService;
public void startTraining() {
YoloConfig config = new YoloConfig();
config.setTaskType("detect");
config.setModelName("yolov8n");
config.setDatasetPath("/path/to/dataset.yaml");
config.setEpochs(100);
config.setBatchSize(16);
config.setImageSize(640);
config.setOutputPath("/path/to/output");
String logFilePath = "/path/to/training_log.txt";
CompletableFuture<TrainResult> future =
yoloOperationService.trainAsyncWithConfig("python", "yolo", config, logFilePath);
// 异步处理结果
future.thenAccept(result -> {
if ("completed".equals(result.getStatus())) {
log.info("训练成功完成");
log.info("导出路径: {}", result.getExportPath());
log.info("最终指标: {}", result.getMetrics());
} else {
log.error("训练失败: {}", result.getErrorMessage());
}
});
}
}
```
## 参数配置详解
### 任务类型 (taskType)
- **detect**: 目标检测任务
- **classify**: 图像分类任务
- **segment**: 实例分割任务
### 模型配置
**方式1: 预训练模型**
```java
config.setModelName("yolov8n"); // n, s, m, l, x
```
**方式2: 自定义模型**
```java
config.setModelPath("D:/models/custom_model.pt");
```
### 数据集配置
数据集配置文件 (.yaml) 应包含:
- 训练和验证数据路径
- 类别名称和数量
- 数据集路径
```yaml
# dataset.yaml 示例
path: /path/to/dataset
train: images/train
val: images/val
names:
0: person
1: car
2: dog
```
### 训练参数
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| epochs | int | 100 | 训练轮数 |
| batchSize | int | 16 | 批次大小 |
| imageSize | int | 640 | 图像大小 |
| learningRate | double | 0.01 | 学习率 |
| device | String | "0" | 设备配置 |
| optimizer | String | "SGD" | 优化器 |
| workers | int | 8 | 工作线程数 |
| savePeriod | int | -1 | 保存间隔 |
### 设备配置
- `"0"`, `"1"`, `"2"`: 使用指定GPU
- `"0,1"`, `"0,1,2"`: 使用多GPU
- `"cpu"`: 使用CPU
## 日志说明
### 自动过滤
- ✅ 保留:每轮批次信息
- ✅ 保留训练指标mAP, precision, recall
- ❌ 过滤:进度条信息(包含%的行但保留100%
- ✅ 保留:模型保存路径信息
### 日志文件
日志文件包含:
- 训练开始时间
- 每轮训练信息(不包含进度条)
- 最终训练指标
- 模型保存路径
- 训练完成时间
## 训练结果
训练完成后,可以通过 `TrainResult` 对象获取:
```java
TrainResult result = future.get();
// 状态信息
String status = result.getStatus(); // pending, running, completed, failed
int progress = result.getProgress(); // 0-100
String errorMessage = result.getErrorMessage(); // 错误信息(如果有)
// 训练指标
int currentEpoch = result.getCurrentEpoch(); // 当前轮次
double loss = result.getLoss(); // 损失值
double precision = result.getPrecision(); // 精度
double recall = result.getRecall(); // 召回率
double map = result.getMap(); // mAP值
// 输出信息
String exportPath = result.getExportPath(); // 模型导出路径
String metrics = result.getMetrics(); // 详细指标
String trainingSummary = result.getTrainingSummary(); // 训练摘要
```
## 示例场景
### 场景1: 目标检测训练
```java
YoloConfig config = new YoloConfig();
config.setTaskType("detect");
config.setModelName("yolov8s");
config.setDatasetPath("D:/datasets/coco.yaml");
config.setEpochs(100);
config.setBatchSize(16);
config.setImageSize(640);
config.setDevice("0");
```
### 场景2: 图像分类训练
```java
YoloConfig config = new YoloConfig();
config.setTaskType("classify");
config.setModelName("yolov8n-cls.pt");
config.setDatasetPath("D:/datasets/imagenet.yaml");
config.setEpochs(50);
config.setBatchSize(32);
config.setImageSize(224);
config.setDevice("0");
```
### 场景3: 使用自定义模型
```java
YoloConfig config = new YoloConfig();
config.setTaskType("detect");
config.setModelPath("D:/models/my_custom_model.pt");
config.setDatasetPath("D:/datasets/custom.yaml");
config.setEpochs(100);
config.setBatchSize(8);
config.setImageSize(1024);
config.setLearningRate(0.001);
config.setOptimizer("Adam");
```
### 场景4: 多GPU训练
```java
YoloConfig config = new YoloConfig();
config.setTaskType("detect");
config.setModelName("yolov8x");
config.setDatasetPath("/datasets/large_dataset.yaml");
config.setEpochs(100);
config.setBatchSize(32);
config.setImageSize(1280);
config.setDevice("0,1,2,3"); // 使用4个GPU
```
## 常见问题
### Q1: 如何查看训练进度?
A: 日志文件中会显示每轮的训练信息,但不包含进度条。可以通过 `TrainResult` 对象的 `progress` 字段查看总体进度。
### Q2: 如何恢复中断的训练?
A: 设置 `savePeriod` 参数来定期保存模型,然后使用保存的模型文件继续训练:
```java
config.setModelPath("D:/yolo_output/train/weights/last.pt");
config.setEpochs(50); // 继续训练50轮
```
### Q3: 如何调整学习率?
A: 设置 `learningRate` 参数:
```java
config.setLearningRate(0.001); // 降低学习率
```
### Q4: 如何在Linux系统上使用
A: 修改 Python 路径和虚拟环境名称:
```java
String pythonPath = "/usr/bin/python3";
String envName = "yolo_env";
```
### Q5: 日志文件太大怎么办?
A: 该方法已自动过滤进度条信息,大大减少了日志大小。如果还需要进一步减少,可以:
- 增加 `savePeriod` 减少日志输出频率
- 使用 `verbose=False` 参数(需要在命令中添加)
## 注意事项
1. 确保Python虚拟环境已安装YOLO
2. 确保数据集配置文件路径正确
3. 确保输出路径有写入权限
4. GPU训练时确保有足够的显存
5. 大型数据集建议使用更大的batch size和workers
## 技术支持
如有问题,请查看:
1. 日志文件中的错误信息
2. YOLO官方文档
3. 项目README文档

@ -22,6 +22,17 @@ public interface YoloOperationService {
*/
CompletableFuture<YoloConfig> trainAsync(String pythonPath, String envName, YoloConfig config);
/**
* YOLO
*
* @param pythonPath Python
* @param envName
* @param config
* @param logFilePath
* @return
*/
CompletableFuture<YoloOperationServiceImpl.TrainResult> trainAsyncWithConfig(String pythonPath, String envName, YoloConfig config, String logFilePath);
/**
* YOLO
*

@ -6,8 +6,8 @@ import cn.iocoder.yudao.module.annotation.dal.dataobject.train.TrainDO;
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.PythonVirtualEnvServiceImpl;
import cn.iocoder.yudao.module.annotation.service.python.vo.PythonVirtualEnvInfo;
import cn.iocoder.yudao.module.annotation.service.train.TrainService;
import cn.iocoder.yudao.module.annotation.service.traininfo.TrainInfoService;
import cn.iocoder.yudao.module.annotation.service.trainresult.TrainResultService;
@ -17,10 +17,6 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
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;
@ -1749,7 +1745,408 @@ public class YoloOperationServiceImpl implements YoloOperationService {
}
}
/**
*
*
* @param pythonPath Python
* @param envName
* @param config YOLO
* @param logFilePath
* @return Future
*/
@Override
public CompletableFuture<TrainResult> trainAsyncWithConfig(String pythonPath, String envName, YoloConfig config, String logFilePath) {
return CompletableFuture.supplyAsync(() -> {
TrainResult result = new TrainResult();
result.setStatus("running");
result.setProgress(0);
// 生成任务ID
String taskId = UUID.randomUUID().toString();
result.setTaskId(taskId);
if (pythonVirtualEnvService == null){
pythonVirtualEnvService = new PythonVirtualEnvServiceImpl();
}
try {
// 检测虚拟环境
PythonVirtualEnvInfo envInfo = pythonVirtualEnvService.activateVirtualEnv(pythonPath, envName);
// if (!envInfo.getVirtualEnvExists() || !envInfo.getYoloInstalled()) {
// result.setStatus("failed");
// result.setErrorMessage("虚拟环境不存在或未安装YOLO");
// return result;
// }
// 构建训练命令
String command = buildTrainCommandWithConfig(envInfo, config);
log.info("开始YOLO训练新方法命令: {}", command);
// 创建日志文件目录
if (logFilePath != null && !logFilePath.isEmpty()) {
java.io.File logFile = new java.io.File(logFilePath);
java.io.File logDir = logFile.getParentFile();
if (logDir != null && !logDir.exists()) {
logDir.mkdirs();
}
}
// 执行训练
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()));
// 写入日志文件
java.io.PrintWriter logWriter = null;
if (logFilePath != null && !logFilePath.isEmpty()) {
try {
logWriter = new java.io.PrintWriter(new java.io.FileWriter(logFilePath, java.nio.charset.StandardCharsets.UTF_8));
} catch (Exception e) {
log.error("创建日志文件失败: {}", logFilePath, e);
}
}
StringBuilder fullOutput = new StringBuilder();
String stdoutLine;
String stderrLine = null;
// 记录上一轮的epoch用于保存每轮信息
int[] lastSavedEpoch = {0};
// 同时读取标准输出和错误输出
while ((stdoutLine = stdoutReader.readLine()) != null || (stderrLine = stderrReader.readLine()) != null) {
if (stdoutLine != null) {
log.info("YOLO训练日志: {}", stdoutLine);
fullOutput.append(stdoutLine).append("\n");
// 过滤掉包含百分号的进度行但保留100%完成行)
boolean isProgressLine = stdoutLine.contains("%") && !stdoutLine.contains("100%");
if (!isProgressLine || stdoutLine.contains("Results saved to") || stdoutLine.contains("mAP") ||
stdoutLine.contains("precision") || stdoutLine.contains("recall")) {
// 写入日志文件
if (logWriter != null) {
logWriter.println(stdoutLine);
logWriter.flush();
}
}
// 解析训练结果
parseTrainingResult(stdoutLine, config, result, lastSavedEpoch);
detectAndParseResultsSaved(stdoutLine, config, null); // config中有trainId
}
if (stderrLine != null) {
log.info("YOLO训练日志: {}", stderrLine);
fullOutput.append(stderrLine).append("\n");
// 过滤掉包含百分号的进度行
boolean isProgressLine = stderrLine.contains("%") && !stderrLine.contains("100%");
if (!isProgressLine || stderrLine.contains("Results saved to") || stderrLine.contains("mAP") ||
stderrLine.contains("precision") || stderrLine.contains("recall")) {
// 写入日志文件
if (logWriter != null) {
logWriter.println(stderrLine);
logWriter.flush();
}
}
// 解析训练结果
parseTrainingResult(stderrLine, config, result, lastSavedEpoch);
detectAndParseResultsSaved(stderrLine, config, null);
}
}
// 关闭日志文件
if (logWriter != null) {
logWriter.close();
}
int exitCode = process.waitFor();
stdoutReader.close();
stderrReader.close();
if (exitCode == 0) {
result.setStatus("completed");
result.setProgress(100);
result.setLogMessage("训练完成");
// 保存导出文件夹路径
if (config.getOutputPath() != null) {
result.setExportPath(config.getOutputPath());
}
// 解析最终训练结果
parseFinalResults(fullOutput.toString(), config);
result.setMetrics(config.getMetrics());
result.setTrainingSummary(config.getTrainingSummary());
log.info("训练完成,日志文件: {}, 导出路径: {}", logFilePath, result.getExportPath());
} else {
result.setStatus("failed");
result.setErrorMessage("训练失败,退出码: " + exitCode);
result.setLogMessage(fullOutput.toString());
}
} catch (Exception e) {
log.error("YOLO训练异常", e);
result.setStatus("failed");
result.setErrorMessage("训练异常: " + e.getMessage());
}
return result;
}, asyncExecutor);
}
/**
*
*/
private String buildTrainCommandWithConfig(PythonVirtualEnvInfo envInfo, YoloConfig config) {
StringBuilder command = new StringBuilder();
// 构建Python命令
if ("conda".equals(envInfo.getVirtualEnvType())) {
command.append("conda run -n ").append(new File(envInfo.getVirtualEnvPath()).getName()).append(" ");
} else {
// 使用虚拟环境的Python路径可在此处修改为你的Python路径
command.append(envInfo.getVirtualEnvPythonPath()).append(" ");
}
// 规范化路径
String dataPath = config.getDatasetPath() != null ? normalizePath(config.getDatasetPath()) : "";
String outputPath = config.getOutputPath() != null ? normalizePath(config.getOutputPath()) : "runs";
String modelPath = config.getModelPath() != null ? normalizePath(config.getModelPath()) : "";
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("'); ");
}
// 构建训练命令
command.append("model.train(");
// data参数数据集配置文件
command.append("data=r'").append(escapePythonString(dataPath)).append("', ");
// 任务类型(检测或分类)
if (config.getTaskType() != null && !config.getTaskType().isEmpty()) {
command.append("task='").append(config.getTaskType()).append("', ");
}
// epochs参数
command.append("epochs=").append(config.getEpochs()).append(", ");
// batch参数
command.append("batch=").append(config.getBatchSize()).append(", ");
// imgsz参数图像大小
command.append("imgsz=").append(config.getImageSize()).append(", ");
// device参数
command.append("device='").append(config.getDevice()).append("', ");
// 学习率
if (config.getLearningRate() > 0) {
command.append("lr0=").append(config.getLearningRate()).append(", ");
}
// 优化器
if (config.getOptimizer() != null && !config.getOptimizer().isEmpty()) {
command.append("optimizer='").append(config.getOptimizer()).append("', ");
}
// 工作线程数
if (config.getWorkers() > 0) {
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("'");
command.append(")\"");
return command.toString();
}
/**
*
*/
private void parseTrainingResult(String line, YoloConfig config, TrainResult result, int[] lastSavedEpoch) {
try {
String trimmedLine = line.trim();
// 解析Epoch信息
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) {
try {
int currentEpoch = Integer.parseInt(epochParts[0]);
int totalEpochs = Integer.parseInt(epochParts[1]);
config.setCurrentEpoch(currentEpoch);
result.setCurrentEpoch(currentEpoch);
// 计算进度
int progress = (int) ((double) currentEpoch / totalEpochs * 90);
result.setProgress(progress);
config.setProgress(progress);
// 当epoch完成时保存
if (currentEpoch > lastSavedEpoch[0] && currentEpoch <= config.getEpochs()) {
lastSavedEpoch[0] = currentEpoch;
}
} catch (NumberFormatException e) {
// 忽略解析错误
}
}
}
}
}
// 解析损失值和指标(过滤掉进度行)
if (trimmedLine.contains("loss") || trimmedLine.contains("precision") ||
trimmedLine.contains("recall") || trimmedLine.contains("mAP")) {
// 不处理包含百分号的行
if (!trimmedLine.contains("%") || trimmedLine.contains("100%")) {
// 解析loss
if (trimmedLine.contains("loss:") && !trimmedLine.contains("box_loss") &&
!trimmedLine.contains("cls_loss") && !trimmedLine.contains("dfl_loss")) {
try {
String lossStr = trimmedLine.substring(trimmedLine.indexOf("loss:") + 5).trim();
lossStr = lossStr.split(",")[0].trim();
double loss = Double.parseDouble(lossStr);
config.setLoss(loss);
result.setLoss(loss);
} catch (Exception e) {
// 忽略解析错误
}
}
// 解析precision
if (trimmedLine.contains("precision:")) {
try {
String precisionStr = trimmedLine.substring(trimmedLine.indexOf("precision:") + 10).trim();
precisionStr = precisionStr.split(",")[0].trim();
double precision = Double.parseDouble(precisionStr);
config.setPrecision(precision);
result.setPrecision(precision);
} catch (Exception e) {
// 忽略解析错误
}
}
// 解析recall
if (trimmedLine.contains("recall:")) {
try {
String recallStr = trimmedLine.substring(trimmedLine.indexOf("recall:") + 7).trim();
recallStr = recallStr.split(",")[0].trim();
double recall = Double.parseDouble(recallStr);
config.setRecall(recall);
result.setRecall(recall);
} catch (Exception e) {
// 忽略解析错误
}
}
// 解析mAP
if (trimmedLine.contains("mAP50-95") || (trimmedLine.contains("mAP") && !trimmedLine.contains("mAP50"))) {
try {
String mapStr = trimmedLine.substring(trimmedLine.indexOf(":") + 1).trim();
mapStr = mapStr.split(",")[0].trim();
double map = Double.parseDouble(mapStr);
config.setMap(map);
result.setMap(map);
} catch (Exception e) {
// 忽略解析错误
}
}
}
}
// 解析任务类型
if (trimmedLine.contains("Classification") || trimmedLine.contains("classify")) {
config.setTaskType("classify");
result.setTaskType("classify");
} else if (trimmedLine.contains("Detection") || trimmedLine.contains("detect")) {
config.setTaskType("detect");
result.setTaskType("detect");
}
} catch (Exception e) {
log.debug("解析训练结果时出错: {}", line, e);
}
}
/**
*
*/
public static class TrainResult {
private String taskId;
private String status;
private int progress;
private String logMessage;
private String errorMessage;
private String metrics;
private String trainingSummary;
private String exportPath;
private int currentEpoch;
private double loss;
private double precision;
private double recall;
private double map;
private String taskType;
// Getters and Setters
public String getTaskId() { return taskId; }
public void setTaskId(String taskId) { this.taskId = taskId; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public int getProgress() { return progress; }
public void setProgress(int progress) { this.progress = progress; }
public String getLogMessage() { return logMessage; }
public void setLogMessage(String logMessage) { this.logMessage = logMessage; }
public String getErrorMessage() { return errorMessage; }
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
public String getMetrics() { return metrics; }
public void setMetrics(String metrics) { this.metrics = metrics; }
public String getTrainingSummary() { return trainingSummary; }
public void setTrainingSummary(String trainingSummary) { this.trainingSummary = trainingSummary; }
public String getExportPath() { return exportPath; }
public void setExportPath(String exportPath) { this.exportPath = exportPath; }
public int getCurrentEpoch() { return currentEpoch; }
public void setCurrentEpoch(int currentEpoch) { this.currentEpoch = currentEpoch; }
public double getLoss() { return loss; }
public void setLoss(double loss) { this.loss = loss; }
public double getPrecision() { return precision; }
public void setPrecision(double precision) { this.precision = precision; }
public double getRecall() { return recall; }
public void setRecall(double recall) { this.recall = recall; }
public double getMap() { return map; }
public void setMap(double map) { this.map = map; }
public String getTaskType() { return taskType; }
public void setTaskType(String taskType) { this.taskType = taskType; }
}
}

@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.annotation.service.yolo;
/**
* YOLO
*/
public class YoloTrainConfig {
private String modelPath; // 模型路径
private String datasetPath; // 数据集路径
private int epochs; // 训练轮数
private int batchSize; // 批次大小
private int imageSize; // 图像大小
private String device; // 设备
private String outputPath; // 输出路径
private String logFilePath; // 日志文件路径
private int projectId; // 项目编号
private String venvPath; // 虚拟环境路径Scripts或bin的上一级
public YoloTrainConfig(String modelPath, String datasetPath, int epochs, int batchSize, int imageSize,
String device, String outputPath, String logFilePath, int projectId, String venvPath) {
this.modelPath = modelPath;
this.datasetPath = datasetPath;
this.epochs = epochs;
this.batchSize = batchSize;
this.imageSize = imageSize;
this.device = device;
this.outputPath = outputPath;
this.logFilePath = logFilePath;
this.projectId = projectId;
this.venvPath = venvPath;
}
// Getters and Setters
public String getModelPath() { return modelPath; }
public String getDatasetPath() { return datasetPath; }
public int getEpochs() { return epochs; }
public int getBatchSize() { return batchSize; }
public int getImageSize() { return imageSize; }
public String getDevice() { return device; }
public String getOutputPath() { return outputPath; }
public String getLogFilePath() { return logFilePath; }
public int getProjectId() { return projectId; }
public String getVenvPath() { return venvPath; }
}

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.annotation.dal.mysql.dataset.DatasetMapper">
<!--
一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
文档可见https://www.iocoder.cn/MyBatis/x-plugins/
-->
</mapper>

@ -11,7 +11,12 @@
<update id="updateByIdSetStatus">
UPDATE annotation_mark
SET status = #{status}
<set>
<if test="status != null">status = #{status},</if>
<if test="path != null">path = #{path},</if>
<if test="imagePath != null">image_path = #{imagePath},</if>
<if test="labelPath != null">label_path = #{labelPath},</if>
</set>
WHERE id = #{id}
</update>
</mapper>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.annotation.dal.mysql.model.ModelMapper">
<!--
一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
文档可见https://www.iocoder.cn/MyBatis/x-plugins/
-->
</mapper>

@ -0,0 +1,128 @@
package cn.iocoder.yudao.module.annotation.service.model;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.annotation.controller.admin.model.vo.ModelPageReqVO;
import cn.iocoder.yudao.module.annotation.controller.admin.model.vo.ModelSaveReqVO;
import cn.iocoder.yudao.module.annotation.dal.dataobject.model.ModelDO;
import cn.iocoder.yudao.module.annotation.dal.mysql.model.ModelMapper;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.module.annotation.enums.ErrorCodeConstants.MODEL_NOT_EXISTS;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link ModelServiceImpl}
*
* @author
*/
@Import(ModelServiceImpl.class)
public class ModelServiceImplTest extends BaseDbUnitTest {
@Resource
private ModelServiceImpl modelService;
@Resource
private ModelMapper modelMapper;
@Test
public void testCreateModel_success() {
// 准备参数
ModelSaveReqVO createReqVO = randomPojo(ModelSaveReqVO.class).setId(null);
// 调用
Integer modelId = modelService.createModel(createReqVO);
// 断言
assertNotNull(modelId);
// 校验记录的属性是否正确
ModelDO model = modelMapper.selectById(modelId);
assertPojoEquals(createReqVO, model, "id");
}
@Test
public void testUpdateModel_success() {
// mock 数据
ModelDO dbModel = randomPojo(ModelDO.class);
modelMapper.insert(dbModel);// @Sql: 先插入出一条存在的数据
// 准备参数
ModelSaveReqVO updateReqVO = randomPojo(ModelSaveReqVO.class, o -> {
o.setId(dbModel.getId()); // 设置更新的 ID
});
// 调用
modelService.updateModel(updateReqVO);
// 校验是否更新正确
ModelDO model = modelMapper.selectById(updateReqVO.getId()); // 获取最新的
assertPojoEquals(updateReqVO, model);
}
@Test
public void testUpdateModel_notExists() {
// 准备参数
ModelSaveReqVO updateReqVO = randomPojo(ModelSaveReqVO.class);
// 调用, 并断言异常
assertServiceException(() -> modelService.updateModel(updateReqVO), MODEL_NOT_EXISTS);
}
@Test
public void testDeleteModel_success() {
// mock 数据
ModelDO dbModel = randomPojo(ModelDO.class);
modelMapper.insert(dbModel);// @Sql: 先插入出一条存在的数据
// 准备参数
Integer id = dbModel.getId();
// 调用
modelService.deleteModel(id);
// 校验数据不存在了
assertNull(modelMapper.selectById(id));
}
@Test
public void testDeleteModel_notExists() {
// 调用, 并断言异常
assertServiceException(() -> modelService.deleteModel(1), MODEL_NOT_EXISTS);
}
@Test
@Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
public void testGetModelPage() {
// mock 数据
ModelDO dbModel = randomPojo(ModelDO.class, o -> { // 等会查询到
o.setName(null);
o.setCreateTime(null);
o.setType(null);
});
modelMapper.insert(dbModel);
// 测试 name 不匹配
modelMapper.insert(cloneIgnoreId(dbModel, o -> o.setName(null)));
// 测试 createTime 不匹配
modelMapper.insert(cloneIgnoreId(dbModel, o -> o.setCreateTime(null)));
// 测试 type 不匹配
modelMapper.insert(cloneIgnoreId(dbModel, o -> o.setType(null)));
// 准备参数
ModelPageReqVO reqVO = new ModelPageReqVO();
reqVO.setName(null);
reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
reqVO.setType(null);
// 调用
PageResult<ModelDO> pageResult = modelService.getModelPage(reqVO);
// 断言
assertEquals(1, pageResult.getTotal());
assertEquals(1, pageResult.getList().size());
assertPojoEquals(dbModel, pageResult.getList().get(0));
}
}

@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.camera.dal.zlm;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
public class Mp4RecordFileResponse {
private int code;
private Mp4RecordFileData data;
@Data
public static class Mp4RecordFileData {
private List<String> paths;
private String rootPath;
}
}

@ -67,7 +67,7 @@ public class FileConfigDO extends BaseDO {
@Override
protected Object parse(String json) {
FileClientConfig config = JsonUtils.parseObjectQuietly(json, new TypeReference<>() {});
FileClientConfig config = JsonUtils.parseObjectQuietly(json, new TypeReference<FileClientConfig>() {});
if (config != null) {
return config;
}

@ -69,11 +69,51 @@ public class StockControlController {
// 随行结束
plcService.orderStop(kescEntity.getTaskId());
}
return CommonResult.success("OK");
}
@PostMapping("/action")
@Operation(summary = "随行开始任务")
@ResponseBody
@PermitAll
public CommonResult<String> action(@RequestBody KsecDataInfo kescEntity){
log.info("随行开始任务:{}",kescEntity);
// kescEntity = kescEntity.fromBySRMNumber(kescEntity);
// 随行开始
if (kescEntity.getCmdName().equals("B1")){
plcService.orderStart(kescEntity);
}else if (kescEntity.getCmdName().contains("C")){
// 随行正在进行
plcService.action(kescEntity.getTaskId(),kescEntity.getCmdName());
}else if (kescEntity.getCmdName().equals("B2")){
// 随行结束
plcService.orderStop(kescEntity.getTaskId());
}
return CommonResult.success("OK");
}
@PostMapping("/check")
@Operation(summary = "盘点任务")
@ResponseBody
@PermitAll
public CommonResult<String> check(@RequestBody KsecDataInfo kescEntity){
log.info("盘点任务:{}",kescEntity);
// kescEntity = kescEntity.fromBySRMNumber(kescEntity);
// 开始盘点
if (kescEntity.getCmdName().equals("E1")){
plcService.checkStart(kescEntity);
plcService.check(kescEntity.getTaskId(),kescEntity.getCmdName());
}else if (kescEntity.getCmdName().contains("E2")){
// 到位盘点
plcService.check(kescEntity.getTaskId(),kescEntity.getCmdName());
plcService.checkStop(kescEntity.getTaskId());
}
return CommonResult.success("OK");
}
@PostMapping("/inventory")
@Operation(summary = "盘点任务")
@ResponseBody
@ -107,7 +147,6 @@ public class StockControlController {
return CommonResult.success(kescEntity);
}
@PostMapping("/resultReport")
@Operation(summary = "随行盘点更新状态")
@ResponseBody
@ -116,6 +155,7 @@ public class StockControlController {
log.info("随行盘点更新状态:{}",kescEntity);
return CommonResult.success(kescEntity);
}
@PostMapping("/updateStatus")
@Operation(summary = "随行盘点更新状态")
@ResponseBody
@ -133,11 +173,6 @@ public class StockControlController {
}else if (kescEntity.getCmdName().startsWith("C")){
plcService.action(kescEntity.getTaskId(),kescEntity.getCmdName());
}
return CommonResult.success("OK");
}
}

@ -1,9 +1,10 @@
package cn.iocoder.yudao.module.camera.controller.admin.specificationConf.vo;
import lombok.*;
import java.util.*;
import io.swagger.v3.oas.annotations.media.Schema;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - 品规配置分页 Request VO")
@Data
@ -23,6 +24,9 @@ public class SpecificationConfPageReqVO extends PageParam {
@Schema(description = "最小面积")
private Integer minArea;
@Schema(description = "单层个数")
private Integer layersNumber;
@Schema(description = "截取高度")
private Integer tolerance;

@ -29,6 +29,11 @@ public class SpecificationConfRespVO {
@ExcelProperty("最小面积")
private Integer minArea;
@Schema(description = "单层个数")
@ExcelProperty("单层个数")
private Integer layersNumber;
@Schema(description = "截取高度")
@ExcelProperty("截取高度")
private Integer tolerance;

@ -16,6 +16,10 @@ public class SpecificationConfSaveReqVO {
@Schema(description = "整堆高度单位mm")
private Integer height;
@Schema(description = "单层个数")
private Integer layersNumber;
@Schema(description = "最小面积")
private Integer minArea;

@ -36,6 +36,10 @@ public class SpecificationConfDO extends BaseDO {
*
*/
private Integer minArea;
/**
*
*/
private Integer layersNumber;
/**
*
*/

@ -10,12 +10,20 @@ import cn.iocoder.yudao.module.camera.service.resources.URLResourcesService;
import cn.iocoder.yudao.module.camera.service.scan.ScanService;
import cn.iocoder.yudao.module.system.service.dict.DictDataService;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializer;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
@Slf4j
@ -25,7 +33,25 @@ public class HikFlaskApiService implements ScanService {
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
@Resource
private OkHttpClient client;
private final Gson gson = new Gson();
private final Gson gson;
public HikFlaskApiService() {
// 配置 Gson 支持 Java 8 时间类型
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
this.gson = new GsonBuilder()
.registerTypeAdapter(LocalDate.class, (JsonSerializer<LocalDate>) (date, type, jsonSerializationContext) ->
new JsonPrimitive(date.format(dateFormatter)))
.registerTypeAdapter(LocalDate.class, (JsonDeserializer<LocalDate>) (json, type, jsonDeserializationContext) ->
LocalDate.parse(json.getAsString(), dateFormatter))
.registerTypeAdapter(LocalDateTime.class, (JsonSerializer<LocalDateTime>) (dateTime, type, jsonSerializationContext) ->
new JsonPrimitive(dateTime.format(dateTimeFormatter)))
.registerTypeAdapter(LocalDateTime.class, (JsonDeserializer<LocalDateTime>) (json, type, jsonDeserializationContext) ->
LocalDateTime.parse(json.getAsString(), dateTimeFormatter))
.create();
}
@Resource

@ -96,6 +96,9 @@ public class PLCServiceImpl implements PLCService {
public void gyrateCameraByCode(CameraDO cameraDO, String code) {
if (cameraDO == null){
return;
}
CameraIoDO cameraIoDO = cameraIoService.getOne(new QueryWrapper<CameraIoDO>()
.eq("camera_Id", cameraDO.getId())
.eq("code", code)
@ -227,20 +230,20 @@ public class PLCServiceImpl implements PLCService {
log.info("request json: " + jsonString);
// 发起 POST 请求
ResponseEntity<String> response = restTemplate.exchange(
url, // 替换为你的 API 地址
HttpMethod.POST,
entity,
String.class
);
// ResponseEntity<String> response = restTemplate.exchange(
// url, // 替换为你的 API 地址
// HttpMethod.POST,
// entity,
// String.class
// );
// 输出响应结果
log.info("Status Code: " + response.getStatusCode());
log.info("Response Body: " + response.getBody());
if (!response.getStatusCode().is2xxSuccessful()) {
setHttpUpdateStatus(url, resultMap, count);
}
// log.info("Status Code: " + response.getStatusCode());
// log.info("Response Body: " + response.getBody());
//
// if (!response.getStatusCode().is2xxSuccessful()) {
// setHttpUpdateStatus(url, resultMap, count);
// }
} catch (Exception e) {
e.printStackTrace();
setHttpUpdateStatus(url, resultMap, count);
@ -315,9 +318,17 @@ public class PLCServiceImpl implements PLCService {
} else if (picCmd.startsWith("load")) {
needCapture = true;
} else {
} else if (picCmd.equals("C1")){
needCapture = true;
}else if (picCmd.equals("C2")){
needCapture = true;
return;
}else if (picCmd.equals("C3")){
needCapture = true;
direction = order.getToDirection();
}else if (picCmd.equals("C4")){
needCapture = true;
direction = order.getToDirection();
}
String saveApiPath = dictDataService.parseDictData("base_conf", "data_api_path").getValue();
@ -343,6 +354,61 @@ public class PLCServiceImpl implements PLCService {
//转向原点位
// gyrateCameraByCode(camera, "C5");
}
//
//
// @Override
// public void action( String cmd) {
// OrderDO order = orderMapper.selectOne(new LambdaQueryWrapper<OrderDO>()
// .eq(OrderDO::getTaskId, taskId)
// .orderByDesc(OrderDO::getCreateTime)
// .last("limit 1"));
// StreetDO street = streetService.getStreetByPlcId(order.getSrmNumber());
// if (street == null) {
// log.error("street not found ,plcId :{}", order.getSrmNumber());
// return;
// }
// //OrderInfo orderInfo = new OrderInfo(street, plcCmdInfo, times, code);
// //判断是否拍照
// boolean needCapture = false;
// String picCmd = cmd;
// Integer direction = order.getFromDirection();
// if (picCmd.endsWith("ing")){
// return;
// }
// if (picCmd.startsWith("unload") ) {
// needCapture = true;
// direction = order.getToDirection();
// } else if (picCmd.startsWith("load")) {
// needCapture = true;
//
// } else {
// return;
//
// }
// String saveApiPath = dictDataService.parseDictData("base_conf", "data_api_path").getValue();
//
//// 判断巷道有几个相机 如果只有一个,则调用一个,如果是两个,先判断是否有对拍设置决定那个相机拍摄
// CameraDO camera = getCameraByLeftRight(street, direction);
//// 调用预置点位
// gyrateCameraByCode(camera, picCmd);
//// int time = Integer.parseInt(dictDataService.parseDictData("camera_conf", "delay_preset_time").getValue());
//// try {
//// Thread.sleep(time);
//// } catch (InterruptedException e) {
//// e.printStackTrace();
//// }
// KsecDataInfo dataInfoData = BeanUtils.toBean(order, KsecDataInfo.class);
// if (needCapture) {
// String pathSrc = PathUtil.createFileName(dataInfoData, street, picCmd, ".jpg");
// pathSrc = cameraCapture(camera, true, pathSrc, dictDataService.getDictDataList("camera_conf"));
//
// urlResourcesService.save(URLResourcesDo.builder().url(saveApiPath + pathSrc).uuid(order.getUuid()).type("1").little("球机拍照").build());
// order.setPics(Strings.hasText(order.getPics()) ? order.getPics() + ";" + saveApiPath + pathSrc : saveApiPath + pathSrc);
// orderMapper.updateById(order);
// }
// //转向原点位
//// gyrateCameraByCode(camera, "C5");
// }
@Resource
URLResourcesService urlResourcesService;
@ -356,7 +422,7 @@ public class PLCServiceImpl implements PLCService {
@Override
public void checkStart(KsecDataInfo dataInfo) {
dataInfo.setCountNumber("true");
dataInfo = dataInfo.toWMSData();
// dataInfo = dataInfo.toWMSData();
String saveApiPath = dictDataService.parseDictData("base_conf", "data_api_path").getValue();
@ -395,14 +461,25 @@ public class PLCServiceImpl implements PLCService {
} else {
newStock.setStreetId(street.getId());
newStock.setCheckNum(dataInfo.getTaskId());
newStock.setStreetId(street.getId());
newStock.setDirection(dataInfo.getFromDirection());
newStock.setSide(dataInfo.getFromSide());
newStock.setSeparation(dataInfo.getFromSeparation());
newStock.setRow(dataInfo.getFromRow());
newStock.setColumn(dataInfo.getFromColumn());
newStock.setTaskId(dataInfo.getTaskId());
newStock.setWmsCode(dataInfo.getWmsCode());
newStock.setWmsTrayCode(dataInfo.getWmsTrayCode());
// newStock.setTrayCode(dataInfo.getTrayCode());
newStock.setWmsCategory(dataInfo.getWmsCategory());
newStock.setWmsCount(dataInfo.getWmsCount());
newStock.setWmsItemCode(dataInfo.getWmsItemCode());
newStock.setWmsShelfCode(dataInfo.getWmsShelfCode());
newStock.setWmsPltCode(dataInfo.getWmsPltCode());
newStock.setWmsCountNumber(dataInfo.getWmsCountNumber());
newStock.setId(stock.getId());
stock = newStock;
stock.setStatus("0");
@ -446,14 +523,14 @@ public class PLCServiceImpl implements PLCService {
public void check(String taskId, String cmd) {
//根据扫码配置 获取扫码结果
Map<String, DictDataDO> dictDataList = dictDataService.getDictDataList("scan_conf");
Map<String, DictDataDO> csscStartList = dictDataService.getDictDataList("cssc_scan");
Map<String, DictDataDO> csscStartList = dictDataService.getDictDataList("scan_enable");
// 如果存在扫码情况,进行扫码
if (csscStartList.get(cmd) != null) {
String[] scanTypes = csscStartList.get(cmd).getValue().split(";");
String[] scanTypes = csscStartList.get(cmd).getValue().split(",");
StockDO stock = stockService.getOne(
new QueryWrapper<StockDO>()
.eq("task_id", taskId)
.last("limit 1")
.last("limit 1").orderByDesc("create_time")
);
if (stock == null) {
log.error("taskId:{}不存在", taskId);
@ -519,7 +596,7 @@ public class PLCServiceImpl implements PLCService {
Map<String, DictDataDO> dictDataList = dictDataService.getDictDataList("base_conf");
Map<String, DictDataDO> csscStartList = dictDataService.getDictDataList("cssc_scan");
Map<String, DictDataDO> csscStartList = dictDataService.getDictDataList("scan_enable");
StockDO stock = stockService.getOne(
new QueryWrapper<StockDO>()
.eq("task_id", taskId)
@ -661,11 +738,11 @@ public class PLCServiceImpl implements PLCService {
if (street.getCamera1Id() != null) {
String path = zLMediaKitService.startRecord("live", street.getCamera1Id().toString());
zLMediaKitService.startRecord("live", street.getCamera1Id().toString());
// String path = cameraVideo(street.getCamera1Id(), pathSrc, order.getCreateTime(), endDownLoadTime, dictDataList);
}
if (street.getCamera2Id() != null) {
String path = zLMediaKitService.startRecord("live", street.getCamera2Id().toString());
zLMediaKitService.startRecord("live", street.getCamera2Id().toString());
}
orderMapper.insert(order);
@ -675,7 +752,6 @@ public class PLCServiceImpl implements PLCService {
@Resource
ZLMediaKitService zLMediaKitService;
public void orderStop(String taskId) {
Map<String, DictDataDO> dictDataList = dictDataService.getDictDataList("camera_conf");
LocalDateTime endTime = LocalDateTime.now();

@ -0,0 +1,85 @@
package cn.iocoder.yudao.module.camera.service.scan.LxService;
import lombok.Data;
/**
*
*/
@Data
public class BoxCountRequest {
private Integer cameraId;
private String cameraCalibratePath;
/**
* PCD
*/
private String pcdFilePath;
/**
* mm
*/
private Double floorHeight;
/**
* mm
*/
private Double boxLength;
/**
* mm
*/
private Double boxWidth;
/**
* mm
*/
private Double boxHeight;
/**
* "1w 2h" 12
* {}w {}h
*/
private String stackType;
/**
*
*/
private Integer maxBoxesPerLayer;
/**
* mm50mm
*/
private Double baseTolerance;
/**
* mm20mm
*/
private Double additionalTolerancePerLevel;
/**
* [minX, minY, minZ]
*/
private double[] minBounds;
/**
* [maxX, maxY, maxZ]
*/
private double[] maxBounds;
/**
*
*/
public static BoxCountRequest createDefault() {
BoxCountRequest request = new BoxCountRequest();
request.setFloorHeight(0.0);
request.setBoxLength(200.0); // 默认20cm
request.setBoxWidth(140.0); // 默认14cm
request.setBoxHeight(250.0); // 默认25cm
request.setStackType("1w"); // 默认单层宽排列
request.setMaxBoxesPerLayer(10); // 默认每层最多10个
request.setBaseTolerance(50.0); // 默认50mm冗余
request.setAdditionalTolerancePerLevel(20.0); // 默认每层增加20mm
return request;
}
}

@ -0,0 +1,96 @@
package cn.iocoder.yudao.module.camera.service.scan.LxService;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
*
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BoxCountResponse {
/**
*
*/
private boolean success;
/**
*
*/
private int totalBoxCount;
/**
* 1
*/
private Map<Integer, Integer> boxesPerLayer;
/**
*
*/
private Map<Integer, Integer> pointsPerLayer;
/**
* mm²
*/
private Map<Integer, Double> areaPerLayer;
/**
*
*/
private int layerCount;
/**
*
*/
private int maxLayer;
/**
* 使PCD
*/
private String pcdFilePath;
/**
*
*/
private long calculationTime;
/**
*
*/
private double floorHeight;
/**
* [, , ] (mm)
*/
private double[] boxDimensions;
/**
*
*/
private String stackType;
/**
*
*/
private String errorMessage;
private String imagePath;
private String result;
/**
*
*/
public static BoxCountResponse error(String errorMessage) {
return BoxCountResponse.builder()
.success(false)
.errorMessage(errorMessage)
.build();
}
}

@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.camera.service.scan.LxService;
import cn.iocoder.yudao.module.camera.dal.dataobject.resources.URLResourcesDo;
import cn.iocoder.yudao.module.camera.dal.dataobject.specificationConf.SpecificationConfDO;
import cn.iocoder.yudao.module.camera.dal.dataobject.stock.StockDO;
import cn.iocoder.yudao.module.camera.dal.dataobject.street.StreetDO;
import cn.iocoder.yudao.module.camera.dal.entity.ScanData;
import cn.iocoder.yudao.module.camera.service.resources.URLResourcesService;
import cn.iocoder.yudao.module.camera.service.specificationConf.SpecificationConfService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Slf4j
@Service
public class LxServiceApi {
@Resource
SpecificationConfService specificationConfService;
@Resource
RestTemplate restTemplate;
@Resource
URLResourcesService urlResourcesService;
BoxCountResponse setHttp(BoxCountRequest request,String url){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
//
BoxCountResponse boxCountResponse = new BoxCountResponse();
HttpEntity<BoxCountRequest> requestHttp = new HttpEntity<>(request, headers);
boxCountResponse = restTemplate.postForObject(url, requestHttp, BoxCountResponse.class);
return boxCountResponse;
}
public ScanData categoryScan(StreetDO streetDO, StockDO stockDO) {
String url = "http://" + streetDO.getPlcIp() + ":" + streetDO.getPlcPort() + "/category/single";
BoxCountRequest request = new BoxCountRequest();
request.setCameraId(1);
BoxCountResponse boxCountResponse = setHttp(request,url);
log.info("LxServiceApi categoryScan:{}",boxCountResponse);
ScanData scanData = new ScanData();
scanData.setCode(boxCountResponse.getResult());
urlResourcesService.save(URLResourcesDo.builder().url(boxCountResponse.getImagePath()).uuid( stockDO.getCheckPic()).type("1").little("品规拍照").build());
return scanData;
}
public ScanData countScan(StreetDO streetDO, StockDO stockDO) {
String url = "http://" + streetDO.getPlcIp() + ":" + streetDO.getPlcPort() + "/api/boxCount/calculate";
SpecificationConfDO specificationConfDO = specificationConfService.getOne(new LambdaQueryWrapper<SpecificationConfDO>().eq(SpecificationConfDO::getType, stockDO.getWmsItemCode()));
BoxCountRequest request = new BoxCountRequest();
request.setBoxHeight(Double.valueOf(specificationConfDO.getHeight()));
request.setBoxLength(Double.valueOf(specificationConfDO.getLength()));
request.setBoxWidth(Double.valueOf(specificationConfDO.getWidth()));
request.setMaxBoxesPerLayer(specificationConfDO.getLayersNumber());
BoxCountResponse boxCountResponse = setHttp(request,url);
log.info("LxServiceApi countScan:{}",boxCountResponse);
ScanData scanData = new ScanData();
scanData.setCode(boxCountResponse.getResult());
urlResourcesService.save(URLResourcesDo.builder().url(boxCountResponse.getImagePath()).uuid( stockDO.getCheckPic()).type("1").little("品规拍照").build());
return scanData;
}
}

@ -33,4 +33,21 @@ public class PCDServiceImpl implements ScanService{
return restTemplate.postForObject(url, request, ScanData.class);
}
public ScanData scanPcd(StreetDO streetDO, StockDO stockDO) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
//
ScanData scanData = new ScanData();
scanData.setCode(stockDO.getCategory());
scanData.setIp(streetDO.getPlcIp());
scanData.setPort(streetDO.getPlcPort());
scanData.setUuid(stockDO.getCheckPic());
scanData.setDirection(stockDO.getDirection());
HttpEntity<ScanData> request = new HttpEntity<>(scanData, headers);
String url = "http://"+streetDO.getPlcIp()+":"+streetDO.getPlcPort()+"/judge/scan";
return restTemplate.postForObject(url, request, ScanData.class);
}
}

@ -4,6 +4,7 @@ import cn.iocoder.yudao.module.camera.dal.dataobject.stock.StockDO;
import cn.iocoder.yudao.module.camera.dal.dataobject.street.StreetDO;
import cn.iocoder.yudao.module.camera.dal.entity.ScanData;
import cn.iocoder.yudao.module.camera.service.Hik3D.HikFlaskApiService;
import cn.iocoder.yudao.module.camera.service.scan.LxService.LxServiceApi;
import cn.iocoder.yudao.module.camera.service.scan.yoloCameraApi.YoloServiceImpl;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@ -27,6 +28,9 @@ public class ScanServiceFactory {
@Resource
private QRCodeScanServiceImpl qrCodeScanService;
@Resource
LxServiceApi lxServiceApi;
// 返回的ScanData的code会将作为扫描结果如果和wms的一致则记录为成功
// 图片保存将在scan方法中实现
@ -73,14 +77,13 @@ public class ScanServiceFactory {
case "9":
log.info("yolo识别扫码-检测");
return yoloService.scanDetect(streetDO, stockDO);
// hik3d识别扫码存在
case "10":
log.info("yolo识别扫码-球机检测");
return yoloService.scanCameraDetect(streetDO, stockDO);
// 二维码识别
log.info("lx接口品规");
return lxServiceApi.categoryScan(streetDO, stockDO);// 二维码识别
case "11":
log.info("二维码识别");
return qrCodeScanService.scan(streetDO, stockDO);
log.info("lx接口个数");
return lxServiceApi.countScan(streetDO, stockDO);
// 默认情况处理
default:
return new ScanData();

@ -8,13 +8,16 @@ import cn.iocoder.yudao.module.camera.service.resources.URLResourcesService;
import cn.iocoder.yudao.module.camera.service.scan.ScanService;
import cn.iocoder.yudao.module.system.service.dict.DictDataService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.gson.Gson;
import com.google.gson.*;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Objects;
@ -25,7 +28,25 @@ public class YoloServiceImpl implements ScanService {
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
@Resource
private OkHttpClient client;
private final Gson gson = new Gson();
private final Gson gson;
public YoloServiceImpl() {
// 配置 Gson 支持 Java 8 时间类型
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
this.gson = new GsonBuilder()
.registerTypeAdapter(LocalDate.class, (JsonSerializer<LocalDate>) (date, type, jsonSerializationContext) ->
new JsonPrimitive(date.format(dateFormatter)))
.registerTypeAdapter(LocalDate.class, (JsonDeserializer<LocalDate>) (json, type, jsonDeserializationContext) ->
LocalDate.parse(json.getAsString(), dateFormatter))
.registerTypeAdapter(LocalDateTime.class, (JsonSerializer<LocalDateTime>) (dateTime, type, jsonSerializationContext) ->
new JsonPrimitive(dateTime.format(dateTimeFormatter)))
.registerTypeAdapter(LocalDateTime.class, (JsonDeserializer<LocalDateTime>) (json, type, jsonDeserializationContext) ->
LocalDateTime.parse(json.getAsString(), dateTimeFormatter))
.create();
}
@Resource
@ -145,6 +166,37 @@ public class YoloServiceImpl implements ScanService {
// return null;
}
/**
*
* @param streetDO
* @param stockDO
* @return
*/
public ScanData scanningLxApiYolo(StreetDO streetDO, StockDO stockDO) {
ScanData scanData = new ScanData();
String urlPath ="http://127.0.0.1:8097/judge/scan";
try {
// scanData.setCode("有货");
Pojo pojo = new Pojo();
pojo.setStreetId(streetDO.getPlcId());
pojo.setDirection(stockDO.getDirection());
pojo = setHttp(pojo,urlPath+"/yolo/detect");
log.info("lx3d拍照结果"+pojo.toString());
scanData.setCode(pojo.getType());
urlResourcesService.save(URLResourcesDo.builder().url(pojo.getPath()).uuid( stockDO.getCheckPic()).type("1").little("有无拍照").build());
return scanData;
} catch (IOException e) {
log.error("识别异常",e);
System.out.println("识别异常");
e.printStackTrace();
throw new RuntimeException(e);
}
// return null;
}
/**
*

@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.camera.controller.admin.specificationConf.vo.SpecificationConfPageReqVO;
import cn.iocoder.yudao.module.camera.controller.admin.specificationConf.vo.SpecificationConfSaveReqVO;
import cn.iocoder.yudao.module.camera.dal.dataobject.specificationConf.SpecificationConfDO;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.validation.Valid;
/**
@ -11,7 +12,7 @@ import jakarta.validation.Valid;
*
* @author
*/
public interface SpecificationConfService {
public interface SpecificationConfService extends IService<SpecificationConfDO> {
/**
*

@ -9,11 +9,11 @@ import cn.iocoder.yudao.module.camera.dal.dataobject.street.StreetDO;
import cn.iocoder.yudao.module.camera.dal.mysql.specificationConf.SpecificationConfMapper;
import cn.iocoder.yudao.module.camera.dal.mysql.street.StreetMapper;
import cn.iocoder.yudao.module.camera.service.Hik3D.HikFlaskApiService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.io.IOException;
import java.util.List;
/**
@ -23,7 +23,7 @@ import java.util.List;
*/
@Service
@Validated
public class SpecificationConfServiceImpl implements SpecificationConfService {
public class SpecificationConfServiceImpl extends ServiceImpl<SpecificationConfMapper, SpecificationConfDO> implements SpecificationConfService {
@Resource
private SpecificationConfMapper specificationConfMapper;
@ -42,13 +42,13 @@ public class SpecificationConfServiceImpl implements SpecificationConfService {
SpecificationConfDO specificationConf = BeanUtils.toBean(createReqVO, SpecificationConfDO.class);
specificationConfMapper.insert(specificationConf);
List<StreetDO> streetDOList = streetMapper.selectList();
for (StreetDO streetDO : streetDOList) {
try {
hikFlaskApiService.addTemplate(specificationConf.getType(), specificationConf,"http://"+streetDO.getPlcIp()+":5000");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// for (StreetDO streetDO : streetDOList) {
// try {
// hikFlaskApiService.addTemplate(specificationConf.getType(), specificationConf,"http://"+streetDO.getPlcIp()+":5000");
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// }
// 返回
return specificationConf.getId();
}
@ -64,13 +64,13 @@ public class SpecificationConfServiceImpl implements SpecificationConfService {
SpecificationConfDO specificationConfDO = specificationConfMapper.selectById(updateReqVO.getId());
List<StreetDO> streetDOList = streetMapper.selectList();
for (StreetDO streetDO : streetDOList) {
try {
hikFlaskApiService.addTemplate(specificationConfDO.getType(), specificationConfDO,"http://"+streetDO.getPlcIp()+":5000");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// for (StreetDO streetDO : streetDOList) {
// try {
// hikFlaskApiService.addTemplate(specificationConfDO.getType(), specificationConfDO,"http://"+streetDO.getPlcIp()+":5000");
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// }
}
@Override

@ -2,9 +2,11 @@ package cn.iocoder.yudao.module.camera.service.streamingMedia;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.camera.dal.dataobject.camera.CameraDO;
import cn.iocoder.yudao.module.camera.dal.zlm.Mp4RecordFileResponse;
import cn.iocoder.yudao.module.camera.dal.zlm.RtspSessionResponse;
import cn.iocoder.yudao.module.camera.service.channel.CameraChannel;
import cn.iocoder.yudao.module.camera.util.PathUtil;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
import cn.iocoder.yudao.module.system.service.dict.DictDataService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@ -46,42 +48,144 @@ public class ZLMediaKitServiceImpl implements ZLMediaKitService{
@Resource
private DictDataService dictDataService;
/**
* ZLMediaKit Redis
*/
private static class ZlmConfig {
String apiurl;
String secret;
String mp4SavePath;
String mp4SaveApi;
}
/**
* ZLMediaKit Redis
* @return ZLMediaKit
*/
private ZlmConfig getZlmConfig() {
ZlmConfig config = new ZlmConfig();
try {
Map<String, DictDataDO> dictDataMap = dictDataService.getDictDataList("ZLMediaKit_conf");
if (dictDataMap != null) {
config.apiurl = dictDataMap.get("Apiurl") != null ? dictDataMap.get("Apiurl").getValue() : "";
config.secret = dictDataMap.get("secret") != null ? dictDataMap.get("secret").getValue() : "";
config.mp4SavePath = dictDataMap.get("mp4SavePath") != null ? dictDataMap.get("mp4SavePath").getValue() : "";
config.mp4SaveApi = dictDataMap.get("mp4SaveApi") != null ? dictDataMap.get("mp4SaveApi").getValue() : "";
}
} catch (Exception e) {
log.error("获取 ZLMediaKit 配置失败: {}", e.getMessage());
// 返回空配置,后续使用时会检查
}
return config;
}
@Override
public String startRecord(String app, String cameraId) {
ZlmConfig config = getZlmConfig();
String zlmApiSecret = dictDataService.parseDictData("ZLMediaKit_conf", "secret").getValue();
String mp4SavePath = dictDataService.parseDictData("ZLMediaKit_conf", "mp4SavePath").getValue();
Map<String, Object> addParams = new HashMap<>();
addParams.put("secret", zlmApiSecret);
addParams.put("secret", config.secret);
addParams.put("type", "1");//0为hls1为mp4
addParams.put("vhost", "__defaultVhost__");
addParams.put("app", "live");
addParams.put("customized_path", mp4SavePath);
addParams.put("customized_path", config.mp4SavePath);
addParams.put("stream","camera"+ cameraId);
// addParams.put("url", CameraChannel.getRtspUrl(camera.getIp(), camera.getRtspPort(), camera.getChannel(), camera.getUser(), camera.getPassword(),camera.getType()));
// sendHttp(addParams, "startRecord");
// 查看是否录像,为录像则重新出发录像
// 重试机制最多尝试6次启动录制
boolean recordStarted = false;
for (int i = 0; i <=5; i++) {
RtspSessionResponse rtspSessionResponse = sendHttp(addParams, "startRecord");
if (rtspSessionResponse != null && rtspSessionResponse.isResult()){
recordStarted = true;
break;
}
// 如果失败,短暂休眠后重试
try {
sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("startRecord 等待被中断: camera={}", cameraId);
return "";
}
}
log.info("startRecord camera:"+cameraId);
String path = checkHiddenFilesInDirectory(app,cameraId);
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
Runnable task = () -> {
stopRecord(app,cameraId,LocalDateTime.now());
if (!recordStarted) {
log.error("startRecord 失败重试6次后仍未成功: camera={}", cameraId);
return "";
}
log.info("startRecord 成功: app={}, camera={}", app, cameraId);
// 延迟1秒后使用API获取当前录制的文件信息
try {
sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("startRecord 等待被中断: camera={}", cameraId);
return "";
}
// 获取当前正在录制的文件路径
String recordingPath = getCurrentRecordingFile(app, cameraId);
log.info("当前录制文件: app={}, camera={}, path={}", app, cameraId, recordingPath);
// 设置定时器5分钟后自动停止录制
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
Runnable task = () -> {
stopRecord(app, cameraId, LocalDateTime.now());
};
// 延迟5分钟后执行停止任务不会将录像文件录5分钟
long delay =5;
scheduler.schedule(task, delay, TimeUnit.MINUTES);
return path;
// 延迟5分钟后执行停止任务
scheduler.schedule(task, 5, TimeUnit.MINUTES);
return recordingPath;
}
/**
*
* @param app
* @param cameraId ID
* @return 访
*/
private String getCurrentRecordingFile(String app, String cameraId) {
ZlmConfig config = getZlmConfig();
// 获取当前日期
LocalDate currentDate = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String currentPeriod = currentDate.format(formatter);
// 调用 getMp4RecordFile API 获取当前日期的录像文件列表
Mp4RecordFileResponse response = queryMp4RecordFile(config.apiurl, config.secret, app, cameraId, currentPeriod, config.mp4SavePath);
if (response != null && response.getCode() == 0 && response.getData() != null
&& response.getData().getPaths() != null && !response.getData().getPaths().isEmpty()) {
// 获取最新的文件(列表中第一个就是最新的)
String fileName = response.getData().getPaths().get(0);
// 检查是否是隐藏文件(以.开头)
if (fileName.startsWith(".")) {
// 隐藏文件(正在录制中),返回完整的访问路径
return config.mp4SaveApi + "record/" + app + "/camera" + cameraId + "/" + currentPeriod + "/" + fileName;
} else {
// 正常文件(已完成录制)
return config.mp4SaveApi + "record/" + app + "/camera" + cameraId + "/" + currentPeriod + "/" + fileName;
}
}
// 如果当前日期没有文件,尝试前一天(处理跨天情况)
LocalDate yesterdayDate = currentDate.minusDays(1);
String yesterdayPeriod = yesterdayDate.format(formatter);
response = queryMp4RecordFile(config.apiurl, config.secret, app, cameraId, yesterdayPeriod, config.mp4SavePath);
if (response != null && response.getCode() == 0 && response.getData() != null
&& response.getData().getPaths() != null && !response.getData().getPaths().isEmpty()) {
String fileName = response.getData().getPaths().get(0);
return config.mp4SaveApi + "record/" + app + "/camera" + cameraId + "/" + yesterdayPeriod + "/" + fileName;
}
log.warn("未找到录制文件: app={}, camera={}", app, cameraId);
return "";
}
// 检查指定文件夹中最新的文件名,且文件修改时间在指定时间之后
@ -128,24 +232,21 @@ public class ZLMediaKitServiceImpl implements ZLMediaKitService{
}
// 检查摄像头目录中最新的文件名,且文件修改时间在指定时间之后
public String getLatestCameraFileNameAfterTime(String app, String cameraId, LocalDateTime afterTime) {
String mp4SavePath = dictDataService.parseDictData("ZLMediaKit_conf", "mp4SavePath").getValue();
ZlmConfig config = getZlmConfig();
String mp4SaveApi = dictDataService.parseDictData("ZLMediaKit_conf", "mp4SaveApi").getValue();
// 获取当前日期
LocalDate currentDate = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String formattedDate = currentDate.format(formatter);
String directoryPath = mp4SavePath + "record/" + app + "/" + cameraId + "/" + formattedDate;
String mp4SaveApiPath = mp4SaveApi + "record/" + app + "/" + cameraId + "/" + formattedDate + "/";
String directoryPath = config.mp4SavePath + "record/" + app + "/" + cameraId + "/" + formattedDate;
String mp4SaveApiPath = config.mp4SaveApi + "record/" + app + "/" + cameraId + "/" + formattedDate + "/";
return mp4SaveApiPath + getLatestFileNameAfterTime(directoryPath, afterTime);
}
// 新增方法:检查指定文件夹下是否有以.为开头的文件
public String checkHiddenFilesInDirectory(String app, String cameraId) {
ZlmConfig config = getZlmConfig();
String mp4SavePath = dictDataService.parseDictData("ZLMediaKit_conf", "mp4SavePath").getValue();
// mp4SaveApi
String mp4SaveApi = dictDataService.parseDictData("ZLMediaKit_conf", "mp4SaveApi").getValue();
// 获取当前日期
LocalDate currentDate = LocalDate.now();
@ -155,8 +256,7 @@ public class ZLMediaKitServiceImpl implements ZLMediaKitService{
// 格式化日期
String formattedDate = currentDate.format(formatter);
String directoryPath = mp4SavePath+"record/"+app+"/camera"+cameraId+"/"+formattedDate;
String directoryPath = config.mp4SavePath + "record/"+app+"/camera"+cameraId+"/"+formattedDate;
Path directory = Paths.get(directoryPath);
List<String> hiddenFiles = new ArrayList<>();
@ -168,7 +268,7 @@ public class ZLMediaKitServiceImpl implements ZLMediaKitService{
.map(Path::toString)
.filter(name -> name.startsWith("."))
.collect(Collectors.toList());
return hiddenFiles.isEmpty() ? "" : mp4SaveApi+"record/"+app+"/camera"+cameraId+"/"+formattedDate+"/"+removeLeadingDot(hiddenFiles.get(0));
return hiddenFiles.isEmpty() ? "" : config.mp4SaveApi+"record/"+app+"/camera"+cameraId+"/"+formattedDate+"/"+removeLeadingDot(hiddenFiles.get(0));
} catch (IOException e) {
e.printStackTrace();
hiddenFiles= List.of(); // 返回空列表
@ -178,10 +278,8 @@ public class ZLMediaKitServiceImpl implements ZLMediaKitService{
// 新增方法:检查指定文件夹下是否有以.为开头的文件
public String checkAndRenameFile(String app, String cameraId,String filePath) {
ZlmConfig config = getZlmConfig();
String mp4SavePath = dictDataService.parseDictData("ZLMediaKit_conf", "mp4SavePath").getValue();
// mp4SaveApi
String mp4SaveApi = dictDataService.parseDictData("ZLMediaKit_conf", "mp4SaveApi").getValue();
// 获取当前日期
LocalDate currentDate = LocalDate.now();
@ -191,11 +289,11 @@ public class ZLMediaKitServiceImpl implements ZLMediaKitService{
// 格式化日期
String formattedDate = currentDate.format(formatter);
// 构建完整路径
String fullPath = mp4SavePath + "record/" + app + "/camera" + cameraId + "/" + formattedDate + "/." + filePath;
String renamedPath = mp4SavePath + "record/" + app + "/camera" + cameraId + "/" + formattedDate + "/" + filePath;
String fullPath = config.mp4SavePath + "record/" + app + "/camera" + cameraId + "/" + formattedDate + "/." + filePath;
String renamedPath = config.mp4SavePath + "record/" + app + "/camera" + cameraId + "/" + formattedDate + "/" + filePath;
// 构建访问路径
String accessPath = mp4SaveApi + "record/" + app + "/camera" + cameraId + "/" + formattedDate + "/" + filePath;
String accessPath = config.mp4SaveApi + "record/" + app + "/camera" + cameraId + "/" + formattedDate + "/" + filePath;
try {
Path originalFilePath = Paths.get(fullPath);
@ -220,21 +318,20 @@ public class ZLMediaKitServiceImpl implements ZLMediaKitService{
* @return 访null
*/
public void captureSnapshot(CameraDO camera, String savePath) {
ZlmConfig config = getZlmConfig();
PathUtil.checkDirc(savePath);
String zlmApiUrl = dictDataService.parseDictData("ZLMediaKit_conf", "Apiurl").getValue();
String zlmApiSecret = dictDataService.parseDictData("ZLMediaKit_conf", "secret").getValue();
try {
// 构建getSnap请求参数
Map<String, Object> snapParams = new HashMap<>();
snapParams.put("secret", zlmApiSecret);
snapParams.put("secret", config.secret);
snapParams.put("vhost", "__defaultVhost__");
snapParams.put("expire_sec",1);
snapParams.put("url",CameraChannel.getRtspUrl(camera.getIp(), camera.getRtspPort(), camera.getChannel(), camera.getUser(), camera.getPassword(),camera.getType()));
snapParams.put("timeout_sec", 10); // 设置超时时间
// 构建请求URL
String snapUrl = buildUrl(zlmApiUrl + "getSnap", null, snapParams);
String snapUrl = buildUrl(config.apiurl + "getSnap", null, snapParams);
// 发送请求并获取截图
Request request = new Request.Builder()
@ -277,40 +374,113 @@ public class ZLMediaKitServiceImpl implements ZLMediaKitService{
}
@Override
public String stopRecord(String app, String stream ,LocalDateTime time) {
ZlmConfig config = getZlmConfig();
String zlmApiSecret = dictDataService.parseDictData("ZLMediaKit_conf", "secret").getValue();
Map<String, Object> addParams = new HashMap<>();
addParams.put("secret", zlmApiSecret);
addParams.put("secret", config.secret);
addParams.put("type", "1");//0为hls1为mp4
addParams.put("vhost", "__defaultVhost__");
addParams.put("app", "live");
addParams.put("stream","camera"+ stream);
// addParams.put("url", CameraChannel.getRtspUrl(camera.getIp(), camera.getRtspPort(), camera.getChannel(), camera.getUser(), camera.getPassword(),camera.getType()));
RtspSessionResponse rtspSessionResponse = sendHttp(addParams, "stopRecord");
if (rtspSessionResponse!=null && rtspSessionResponse.isResult()){
try {
sleep(1000);
// 等待文件写入完成
sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
String fileName = getLatestCameraFileNameAfterTime(app, "camera"+stream, time.minusSeconds(10));
return fileName;
}else {
// 使用 ZLMediaKit API 获取最新录像文件
String recordPath = getLatestMp4FileByApi(app, stream);
return recordPath;
} else {
return "";
}
}
/**
* 使 ZLMediaKit getMp4RecordFile API MP4
* @param app
* @param stream ID
* @return 访
*/
private String getLatestMp4FileByApi(String app, String stream) {
ZlmConfig config = getZlmConfig();
// 获取当前日期和前一天日期(处理跨天情况)
LocalDate currentDate = LocalDate.now();
LocalDate yesterdayDate = currentDate.minusDays(1);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 尝试获取当前日期的录像文件
String currentPeriod = currentDate.format(formatter);
Mp4RecordFileResponse response = queryMp4RecordFile(config.apiurl, config.secret, app, stream, currentPeriod, config.mp4SavePath);
if (response != null && response.getCode() == 0 && response.getData() != null
&& response.getData().getPaths() != null && !response.getData().getPaths().isEmpty()) {
// 获取最新的文件(列表中第一个就是最新的)
String fileName = response.getData().getPaths().get(0);
return config.mp4SaveApi + "record/" + app + "/camera" + stream + "/" + currentPeriod + "/" + fileName;
}
// 如果当前日期没有文件,尝试前一天(处理跨天情况)
String yesterdayPeriod = yesterdayDate.format(formatter);
response = queryMp4RecordFile(config.apiurl, config.secret, app, stream, yesterdayPeriod, config.mp4SavePath);
if (response != null && response.getCode() == 0 && response.getData() != null
&& response.getData().getPaths() != null && !response.getData().getPaths().isEmpty()) {
String fileName = response.getData().getPaths().get(0);
return config.mp4SaveApi + "record/" + app + "/camera" + stream + "/" + yesterdayPeriod + "/" + fileName;
}
log.warn("未找到录像文件: app={}, stream={}", app, stream);
return "";
}
/**
* ZLMediaKit getMp4RecordFile API
* @param zlmApiUrl ZLMediaKit API
* @param zlmApiSecret API
* @param app
* @param stream ID
* @param period yyyy-MM-dd
* @param customizedPath
* @return API
*/
private Mp4RecordFileResponse queryMp4RecordFile(String zlmApiUrl, String zlmApiSecret,
String app, String stream, String period, String customizedPath) {
try {
Map<String, Object> params = new HashMap<>();
params.put("secret", zlmApiSecret);
params.put("vhost", "__defaultVhost__");
params.put("app", app);
params.put("stream", "camera" + stream);
params.put("period", period);
if (customizedPath != null && !customizedPath.isEmpty()) {
params.put("customized_path", customizedPath);
}
String queryUrl = buildUrl(zlmApiUrl + "getMp4RecordFile", null, params);
String response = get(queryUrl);
Mp4RecordFileResponse recordFileResponse = JsonUtils.parseObject(response, Mp4RecordFileResponse.class);
return recordFileResponse;
} catch (Exception e) {
log.error("查询录像文件失败: period={}, 错误: {}", period, e.getMessage());
return null;
}
}
@Override
public void zlmConf( List<CameraDO> list) {
ZlmConfig config = getZlmConfig();
String zlmApiUrl = dictDataService.parseDictData("ZLMediaKit_conf", "Apiurl").getValue();
String zlmApiSecret = dictDataService.parseDictData("ZLMediaKit_conf", "secret").getValue();
try {
// 查询当前的RTSP拉流代理
Map<String, Object> queryParams = new HashMap<>();
queryParams.put("secret", zlmApiSecret);
String queryUrl = buildUrl(zlmApiUrl +"getMediaList",null , queryParams);
queryParams.put("secret", config.secret);
String queryUrl = buildUrl(config.apiurl +"getMediaList",null , queryParams);
String response = get(queryUrl);
RtspSessionResponse rtspSessionResponse = JsonUtils.parseObject(response, RtspSessionResponse.class);
// 检查并添加缺失的RTSP代理
@ -328,7 +498,7 @@ public class ZLMediaKitServiceImpl implements ZLMediaKitService{
if (!isRtspProxyExists){
try {
addRtspProxy(entry, zlmApiUrl, zlmApiSecret);
addRtspProxy(entry, config.apiurl, config.secret);
}catch (Exception e){
e.printStackTrace();
}
@ -368,11 +538,11 @@ public class ZLMediaKitServiceImpl implements ZLMediaKitService{
}
public RtspSessionResponse sendHttp(Map<String, Object> queryParams ,String url){
ZlmConfig config = getZlmConfig();
String zlmApiUrl = dictDataService.parseDictData("ZLMediaKit_conf", "Apiurl").getValue();
try {
// 查询当前的RTSP拉流代理
String queryUrl = buildUrl(zlmApiUrl +url,null , queryParams);
String queryUrl = buildUrl(config.apiurl +url,null , queryParams);
String response = get(queryUrl);
RtspSessionResponse rtspSessionResponse = JsonUtils.parseObject(response, RtspSessionResponse.class);
return rtspSessionResponse;

@ -216,7 +216,7 @@ yudao:
order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
access-log: # 访问日志的配置项
enable: false
enable: true
demo: false # 关闭演示模式
tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc

Loading…
Cancel
Save