You've already forked FrameTour-BE
feat(image): 添加图像超分处理功能
- 新增 ImageSRStage 类实现图像超分辨率处理 - 在 AioDeviceController 中启用图像超分和增强的 Stage - 修改 ImageEnhanceStage 配置检查逻辑,增加空值和占位符检测 - 为图像处理 Pipeline 添加超分 Stage - 增加 ImageSRStage 的单元测试覆盖各种配置和执行情况 - 实现百度云图像超分 API 的调用和结果处理逻辑
This commit is contained in:
@@ -9,6 +9,7 @@ import com.ycwl.basic.image.pipeline.core.PipelineBuilder;
|
|||||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.stages.DownloadStage;
|
import com.ycwl.basic.image.pipeline.stages.DownloadStage;
|
||||||
import com.ycwl.basic.image.pipeline.stages.ImageEnhanceStage;
|
import com.ycwl.basic.image.pipeline.stages.ImageEnhanceStage;
|
||||||
|
import com.ycwl.basic.image.pipeline.stages.ImageSRStage;
|
||||||
import com.ycwl.basic.image.pipeline.stages.SourcePhotoUpdateStage;
|
import com.ycwl.basic.image.pipeline.stages.SourcePhotoUpdateStage;
|
||||||
import com.ycwl.basic.image.pipeline.stages.CleanupStage;
|
import com.ycwl.basic.image.pipeline.stages.CleanupStage;
|
||||||
import com.ycwl.basic.mapper.AioDeviceMapper;
|
import com.ycwl.basic.mapper.AioDeviceMapper;
|
||||||
@@ -155,10 +156,9 @@ public class AioDeviceController {
|
|||||||
photo.getGoodsId(), photo.getUrl(), photo.getScenicId()
|
photo.getGoodsId(), photo.getUrl(), photo.getScenicId()
|
||||||
);
|
);
|
||||||
|
|
||||||
// 启用图像增强Stage
|
// 启用图像增强和超分的Stage
|
||||||
Map<String, Boolean> stageConfig = new HashMap<>();
|
context.enableStage("image_enhance");
|
||||||
stageConfig.put("image_enhance", true);
|
context.enableStage("image_sr");
|
||||||
context.loadStageConfig(null, stageConfig);
|
|
||||||
|
|
||||||
// 执行Pipeline
|
// 执行Pipeline
|
||||||
boolean success = superResolutionPipeline.execute(context);
|
boolean success = superResolutionPipeline.execute(context);
|
||||||
@@ -243,7 +243,7 @@ public class AioDeviceController {
|
|||||||
|
|
||||||
return new PipelineBuilder<PhotoProcessContext>("SourcePhotoSuperResolutionPipeline")
|
return new PipelineBuilder<PhotoProcessContext>("SourcePhotoSuperResolutionPipeline")
|
||||||
.addStage(new DownloadStage()) // 1. 下载图片
|
.addStage(new DownloadStage()) // 1. 下载图片
|
||||||
.addStage(new ImageEnhanceStage(config)) // 2. 图像增强(超分)
|
.addStage(new ImageEnhanceStage(config)).addStage(new ImageSRStage(config)) // 2. 图像增强(超分)
|
||||||
.addStage(new SourcePhotoUpdateStage(sourceService, sourceId)) // 3. 上传并更新数据库
|
.addStage(new SourcePhotoUpdateStage(sourceService, sourceId)) // 3. 上传并更新数据库
|
||||||
.addStage(new CleanupStage()) // 4. 清理临时文件
|
.addStage(new CleanupStage()) // 4. 清理临时文件
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -137,9 +137,15 @@ public class ImageEnhanceStage extends AbstractPipelineStage<PhotoProcessContext
|
|||||||
String apiKey = enhancerConfig.getApiKey();
|
String apiKey = enhancerConfig.getApiKey();
|
||||||
String secretKey = enhancerConfig.getSecretKey();
|
String secretKey = enhancerConfig.getSecretKey();
|
||||||
|
|
||||||
if (appId == null ||
|
// 检查字段是否为 null 或空
|
||||||
apiKey == null ||
|
if (appId == null || appId.isBlank() ||
|
||||||
secretKey == null) {
|
apiKey == null || apiKey.isBlank() ||
|
||||||
|
secretKey == null || secretKey.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否包含 TODO 占位符
|
||||||
|
if (appId.contains("TODO") || apiKey.contains("TODO") || secretKey.contains("TODO")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package com.ycwl.basic.image.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.image.enhancer.adapter.BceImageSR;
|
||||||
|
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
|
||||||
|
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
||||||
|
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
||||||
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
|
import com.ycwl.basic.image.pipeline.core.StageResult;
|
||||||
|
import com.ycwl.basic.image.pipeline.enums.ImageSource;
|
||||||
|
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图像超分辨率Stage
|
||||||
|
* 使用百度云图像超分辨率API提升图片分辨率和清晰度
|
||||||
|
*
|
||||||
|
* 与ImageEnhanceStage的区别:
|
||||||
|
* - ImageEnhanceStage: 使用BceImageEnhancer进行清晰度增强
|
||||||
|
* - ImageSRStage: 使用BceImageSR进行超分辨率处理
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@StageConfig(
|
||||||
|
stageId = "image_sr",
|
||||||
|
optionalMode = StageOptionalMode.SUPPORT,
|
||||||
|
description = "图像超分辨率处理",
|
||||||
|
defaultEnabled = false // 默认不启用,需要外部配置开启
|
||||||
|
)
|
||||||
|
public class ImageSRStage extends AbstractPipelineStage<PhotoProcessContext> {
|
||||||
|
|
||||||
|
private BceEnhancerConfig enhancerConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数 - 使用默认配置
|
||||||
|
*/
|
||||||
|
public ImageSRStage() {
|
||||||
|
this(buildConfigFromEnvironment());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数 - 使用自定义配置
|
||||||
|
*
|
||||||
|
* @param enhancerConfig 图像增强配置
|
||||||
|
*/
|
||||||
|
public ImageSRStage(BceEnhancerConfig enhancerConfig) {
|
||||||
|
this.enhancerConfig = enhancerConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "ImageSRStage";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldExecuteByBusinessLogic(PhotoProcessContext context) {
|
||||||
|
// 仅对照片源为IPC的图片进行超分辨率处理
|
||||||
|
return context.getSource() == ImageSource.IPC;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
|
||||||
|
// 检查配置是否完整
|
||||||
|
if (!isConfigValid()) {
|
||||||
|
log.warn("图像超分辨率配置不完整,跳过处理。请配置百度云API凭证");
|
||||||
|
return StageResult.skipped("配置不完整,跳过超分辨率");
|
||||||
|
}
|
||||||
|
|
||||||
|
File currentFile = context.getCurrentFile();
|
||||||
|
if (currentFile == null || !currentFile.exists()) {
|
||||||
|
return StageResult.skipped("当前文件不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.debug("开始图像超分辨率处理: {}", currentFile.getName());
|
||||||
|
|
||||||
|
// 创建百度云图像超分辨率客户端
|
||||||
|
BceImageSR srEnhancer = new BceImageSR();
|
||||||
|
srEnhancer.setConfig(enhancerConfig);
|
||||||
|
|
||||||
|
// 调用图像超分辨率API
|
||||||
|
// 注意:百度云API需要传入图片URL,这里使用本地文件的绝对路径
|
||||||
|
String imageUrl = currentFile.getAbsolutePath();
|
||||||
|
MultipartFile enhancedImage = srEnhancer.enhance(imageUrl);
|
||||||
|
|
||||||
|
if (enhancedImage == null || enhancedImage.isEmpty()) {
|
||||||
|
log.warn("图像超分辨率返回空结果,可能是API调用失败");
|
||||||
|
return StageResult.degraded("超分辨率失败,使用原图");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存超分辨率后的图片到临时文件
|
||||||
|
File enhancedFile = context.getTempFileManager()
|
||||||
|
.createTempFile("sr_enhanced", ".jpg");
|
||||||
|
|
||||||
|
saveMultipartFileToFile(enhancedImage, enhancedFile);
|
||||||
|
|
||||||
|
if (!enhancedFile.exists() || enhancedFile.length() == 0) {
|
||||||
|
return StageResult.degraded("超分辨率结果保存失败,使用原图");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新处理后的文件
|
||||||
|
context.updateProcessedFile(enhancedFile);
|
||||||
|
|
||||||
|
long originalSize = currentFile.length();
|
||||||
|
long enhancedSize = enhancedFile.length();
|
||||||
|
double sizeRatio = (double) enhancedSize / originalSize;
|
||||||
|
|
||||||
|
log.info("图像超分辨率完成: 原始{}KB -> 超分后{}KB (比例: {})",
|
||||||
|
originalSize / 1024,
|
||||||
|
enhancedSize / 1024,
|
||||||
|
String.format("%.2f", sizeRatio));
|
||||||
|
|
||||||
|
return StageResult.success(String.format("超分辨率完成 (%dKB -> %dKB)",
|
||||||
|
originalSize / 1024,
|
||||||
|
enhancedSize / 1024));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("图像超分辨率失败: {}", e.getMessage(), e);
|
||||||
|
|
||||||
|
// 超分辨率失败时返回降级状态,继续使用原图
|
||||||
|
return StageResult.degraded("超分辨率失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查配置是否有效
|
||||||
|
*/
|
||||||
|
private boolean isConfigValid() {
|
||||||
|
if (enhancerConfig == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String appId = enhancerConfig.getAppId();
|
||||||
|
String apiKey = enhancerConfig.getApiKey();
|
||||||
|
String secretKey = enhancerConfig.getSecretKey();
|
||||||
|
|
||||||
|
// 检查字段是否为 null 或空
|
||||||
|
if (appId == null || appId.isBlank() ||
|
||||||
|
apiKey == null || apiKey.isBlank() ||
|
||||||
|
secretKey == null || secretKey.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否包含 TODO 占位符
|
||||||
|
if (appId.contains("TODO") || apiKey.contains("TODO") || secretKey.contains("TODO")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从环境变量构建配置
|
||||||
|
*/
|
||||||
|
private static BceEnhancerConfig buildConfigFromEnvironment() {
|
||||||
|
BceEnhancerConfig config = new BceEnhancerConfig();
|
||||||
|
config.setAppId(System.getenv("BCE_IMAGE_APP_ID"));
|
||||||
|
config.setApiKey(System.getenv("BCE_IMAGE_API_KEY"));
|
||||||
|
config.setSecretKey(System.getenv("BCE_IMAGE_SECRET_KEY"));
|
||||||
|
config.setQps(1.0f);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存MultipartFile到本地文件
|
||||||
|
*/
|
||||||
|
private void saveMultipartFileToFile(MultipartFile multipartFile, File targetFile) throws IOException {
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(targetFile)) {
|
||||||
|
fos.write(multipartFile.getBytes());
|
||||||
|
fos.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前配置(用于调试)
|
||||||
|
*/
|
||||||
|
public BceEnhancerConfig getEnhancerConfig() {
|
||||||
|
return enhancerConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置配置(用于动态配置)
|
||||||
|
*/
|
||||||
|
public void setEnhancerConfig(BceEnhancerConfig enhancerConfig) {
|
||||||
|
this.enhancerConfig = enhancerConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,456 @@
|
|||||||
|
package com.ycwl.basic.image.pipeline.stages;
|
||||||
|
|
||||||
|
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
|
||||||
|
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||||
|
import com.ycwl.basic.image.pipeline.core.StageResult;
|
||||||
|
import com.ycwl.basic.image.pipeline.enums.ImageSource;
|
||||||
|
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ImageSRStage 单元测试
|
||||||
|
*/
|
||||||
|
class ImageSRStageTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetName() {
|
||||||
|
ImageSRStage stage = new ImageSRStage();
|
||||||
|
|
||||||
|
assertEquals("ImageSRStage", stage.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDefaultConstructor_ShouldSetDefaultConfig() {
|
||||||
|
ImageSRStage stage = new ImageSRStage();
|
||||||
|
|
||||||
|
BceEnhancerConfig config = stage.getEnhancerConfig();
|
||||||
|
|
||||||
|
assertNotNull(config);
|
||||||
|
assertEquals(1.0f, config.getQps());
|
||||||
|
assertEquals(System.getenv("BCE_IMAGE_APP_ID"), config.getAppId());
|
||||||
|
assertEquals(System.getenv("BCE_IMAGE_API_KEY"), config.getApiKey());
|
||||||
|
assertEquals(System.getenv("BCE_IMAGE_SECRET_KEY"), config.getSecretKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testConstructorWithConfig() {
|
||||||
|
BceEnhancerConfig customConfig = new BceEnhancerConfig();
|
||||||
|
customConfig.setAppId("custom-app-id");
|
||||||
|
customConfig.setApiKey("custom-api-key");
|
||||||
|
customConfig.setSecretKey("custom-secret-key");
|
||||||
|
customConfig.setQps(2.0f);
|
||||||
|
|
||||||
|
ImageSRStage stage = new ImageSRStage(customConfig);
|
||||||
|
|
||||||
|
BceEnhancerConfig config = stage.getEnhancerConfig();
|
||||||
|
assertEquals("custom-app-id", config.getAppId());
|
||||||
|
assertEquals("custom-api-key", config.getApiKey());
|
||||||
|
assertEquals("custom-secret-key", config.getSecretKey());
|
||||||
|
assertEquals(2.0f, config.getQps());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testShouldExecute_IPCSource() {
|
||||||
|
ImageSRStage stage = new ImageSRStage();
|
||||||
|
|
||||||
|
PhotoProcessContext context = PhotoProcessContext.builder()
|
||||||
|
.originalUrl("https://example.com/test.jpg")
|
||||||
|
.scenicId(123L)
|
||||||
|
.source(ImageSource.IPC)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 需要显式启用 image_sr
|
||||||
|
context.loadStageConfig(null, java.util.Map.of("image_sr", true));
|
||||||
|
|
||||||
|
assertTrue(stage.shouldExecute(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testShouldExecute_PhoneSource_ShouldSkip() {
|
||||||
|
ImageSRStage stage = new ImageSRStage();
|
||||||
|
|
||||||
|
PhotoProcessContext context = PhotoProcessContext.builder()
|
||||||
|
.originalUrl("https://example.com/test.jpg")
|
||||||
|
.scenicId(123L)
|
||||||
|
.source(ImageSource.PHONE)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
context.loadStageConfig(null, java.util.Map.of("image_sr", true));
|
||||||
|
|
||||||
|
assertFalse(stage.shouldExecute(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testShouldExecute_CameraSource_ShouldSkip() {
|
||||||
|
ImageSRStage stage = new ImageSRStage();
|
||||||
|
|
||||||
|
PhotoProcessContext context = PhotoProcessContext.builder()
|
||||||
|
.originalUrl("https://example.com/test.jpg")
|
||||||
|
.scenicId(123L)
|
||||||
|
.source(ImageSource.CAMERA)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
context.loadStageConfig(null, java.util.Map.of("image_sr", true));
|
||||||
|
|
||||||
|
assertFalse(stage.shouldExecute(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testShouldExecute_UnknownSource_ShouldSkip() {
|
||||||
|
ImageSRStage stage = new ImageSRStage();
|
||||||
|
|
||||||
|
PhotoProcessContext context = PhotoProcessContext.builder()
|
||||||
|
.originalUrl("https://example.com/test.jpg")
|
||||||
|
.scenicId(123L)
|
||||||
|
.source(ImageSource.UNKNOWN)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
context.loadStageConfig(null, java.util.Map.of("image_sr", true));
|
||||||
|
|
||||||
|
assertFalse(stage.shouldExecute(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testShouldExecute_NullSource_ShouldSkip() {
|
||||||
|
ImageSRStage stage = new ImageSRStage();
|
||||||
|
|
||||||
|
PhotoProcessContext context = PhotoProcessContext.builder()
|
||||||
|
.originalUrl("https://example.com/test.jpg")
|
||||||
|
.scenicId(123L)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
context.loadStageConfig(null, java.util.Map.of("image_sr", true));
|
||||||
|
|
||||||
|
assertFalse(stage.shouldExecute(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testShouldExecute_DisabledByConfig_ShouldSkip() {
|
||||||
|
ImageSRStage stage = new ImageSRStage();
|
||||||
|
|
||||||
|
PhotoProcessContext context = PhotoProcessContext.builder()
|
||||||
|
.originalUrl("https://example.com/test.jpg")
|
||||||
|
.scenicId(123L)
|
||||||
|
.source(ImageSource.IPC)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 默认不启用,不加载配置
|
||||||
|
assertFalse(stage.shouldExecute(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_NoCurrentFile_ShouldSkip() {
|
||||||
|
// 使用有效配置,确保配置检查通过
|
||||||
|
BceEnhancerConfig validConfig = new BceEnhancerConfig();
|
||||||
|
validConfig.setAppId("valid-app-id");
|
||||||
|
validConfig.setApiKey("valid-api-key");
|
||||||
|
validConfig.setSecretKey("valid-secret-key");
|
||||||
|
|
||||||
|
ImageSRStage stage = new ImageSRStage(validConfig);
|
||||||
|
|
||||||
|
PhotoProcessContext context = PhotoProcessContext.builder()
|
||||||
|
.originalUrl("https://example.com/test.jpg")
|
||||||
|
.scenicId(123L)
|
||||||
|
.source(ImageSource.IPC)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 不设置文件,让 getCurrentFile() 返回 null
|
||||||
|
StageResult result = stage.execute(context);
|
||||||
|
|
||||||
|
assertTrue(result.isSkipped());
|
||||||
|
assertEquals("当前文件不存在", result.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_InvalidConfig_ShouldSkip() {
|
||||||
|
// 创建无效配置(使用 TODO 占位符)
|
||||||
|
BceEnhancerConfig invalidConfig = new BceEnhancerConfig();
|
||||||
|
invalidConfig.setAppId("TODO_YOUR_APP_ID");
|
||||||
|
invalidConfig.setApiKey("TODO_YOUR_API_KEY");
|
||||||
|
invalidConfig.setSecretKey("TODO_YOUR_SECRET_KEY");
|
||||||
|
|
||||||
|
ImageSRStage stage = new ImageSRStage(invalidConfig);
|
||||||
|
|
||||||
|
PhotoProcessContext context = PhotoProcessContext.builder()
|
||||||
|
.processId("test-invalid-config")
|
||||||
|
.originalUrl("https://example.com/test.jpg")
|
||||||
|
.scenicId(123L)
|
||||||
|
.source(ImageSource.IPC)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 创建一个实际存在的临时文件
|
||||||
|
File tempFile = context.getTempFileManager().createTempFile("test", ".jpg");
|
||||||
|
try (java.io.FileOutputStream fos = new java.io.FileOutputStream(tempFile)) {
|
||||||
|
fos.write(new byte[]{1, 2, 3, 4, 5});
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
context.setOriginalFile(tempFile);
|
||||||
|
|
||||||
|
StageResult result = stage.execute(context);
|
||||||
|
|
||||||
|
assertTrue(result.isSkipped());
|
||||||
|
assertEquals("配置不完整,跳过超分辨率", result.getMessage());
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
context.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_NullConfig_ShouldSkip(@TempDir Path tempDir) throws Exception {
|
||||||
|
ImageSRStage stage = new ImageSRStage(null);
|
||||||
|
|
||||||
|
PhotoProcessContext context = PhotoProcessContext.builder()
|
||||||
|
.processId("test-null-config")
|
||||||
|
.originalUrl("https://example.com/test.jpg")
|
||||||
|
.scenicId(123L)
|
||||||
|
.source(ImageSource.IPC)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 创建一个临时文件
|
||||||
|
File testFile = tempDir.resolve("test.jpg").toFile();
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(testFile)) {
|
||||||
|
fos.write(new byte[]{1, 2, 3, 4, 5});
|
||||||
|
}
|
||||||
|
context.setOriginalFile(testFile);
|
||||||
|
|
||||||
|
StageResult result = stage.execute(context);
|
||||||
|
|
||||||
|
assertTrue(result.isSkipped());
|
||||||
|
assertEquals("配置不完整,跳过超分辨率", result.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_NullAppId_ShouldSkip(@TempDir Path tempDir) throws Exception {
|
||||||
|
BceEnhancerConfig config = new BceEnhancerConfig();
|
||||||
|
config.setAppId(null);
|
||||||
|
config.setApiKey("valid-api-key");
|
||||||
|
config.setSecretKey("valid-secret-key");
|
||||||
|
|
||||||
|
ImageSRStage stage = new ImageSRStage(config);
|
||||||
|
|
||||||
|
PhotoProcessContext context = PhotoProcessContext.builder()
|
||||||
|
.processId("test-null-appid")
|
||||||
|
.originalUrl("https://example.com/test.jpg")
|
||||||
|
.scenicId(123L)
|
||||||
|
.source(ImageSource.IPC)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
File testFile = tempDir.resolve("test.jpg").toFile();
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(testFile)) {
|
||||||
|
fos.write(new byte[]{1, 2, 3, 4, 5});
|
||||||
|
}
|
||||||
|
context.setOriginalFile(testFile);
|
||||||
|
|
||||||
|
StageResult result = stage.execute(context);
|
||||||
|
|
||||||
|
assertTrue(result.isSkipped());
|
||||||
|
assertEquals("配置不完整,跳过超分辨率", result.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_TodoPlaceholderAppId_ShouldSkip(@TempDir Path tempDir) throws Exception {
|
||||||
|
BceEnhancerConfig config = new BceEnhancerConfig();
|
||||||
|
config.setAppId("TODO_APP_ID");
|
||||||
|
config.setApiKey("valid-api-key");
|
||||||
|
config.setSecretKey("valid-secret-key");
|
||||||
|
|
||||||
|
ImageSRStage stage = new ImageSRStage(config);
|
||||||
|
|
||||||
|
PhotoProcessContext context = PhotoProcessContext.builder()
|
||||||
|
.processId("test-todo-appid")
|
||||||
|
.originalUrl("https://example.com/test.jpg")
|
||||||
|
.scenicId(123L)
|
||||||
|
.source(ImageSource.IPC)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
File testFile = tempDir.resolve("test.jpg").toFile();
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(testFile)) {
|
||||||
|
fos.write(new byte[]{1, 2, 3, 4, 5});
|
||||||
|
}
|
||||||
|
context.setOriginalFile(testFile);
|
||||||
|
|
||||||
|
StageResult result = stage.execute(context);
|
||||||
|
|
||||||
|
assertTrue(result.isSkipped(), "应该跳过执行,因为配置包含 TODO 占位符");
|
||||||
|
assertEquals("配置不完整,跳过超分辨率", result.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetAndGetEnhancerConfig() {
|
||||||
|
ImageSRStage stage = new ImageSRStage();
|
||||||
|
|
||||||
|
BceEnhancerConfig newConfig = new BceEnhancerConfig();
|
||||||
|
newConfig.setAppId("new-app-id");
|
||||||
|
newConfig.setApiKey("new-api-key");
|
||||||
|
newConfig.setSecretKey("new-secret-key");
|
||||||
|
newConfig.setQps(3.0f);
|
||||||
|
|
||||||
|
stage.setEnhancerConfig(newConfig);
|
||||||
|
|
||||||
|
BceEnhancerConfig retrieved = stage.getEnhancerConfig();
|
||||||
|
assertEquals("new-app-id", retrieved.getAppId());
|
||||||
|
assertEquals("new-api-key", retrieved.getApiKey());
|
||||||
|
assertEquals("new-secret-key", retrieved.getSecretKey());
|
||||||
|
assertEquals(3.0f, retrieved.getQps());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecute_AllImageTypes_WithIPCSource(@TempDir Path tempDir) throws Exception {
|
||||||
|
ImageSRStage stage = new ImageSRStage();
|
||||||
|
|
||||||
|
// 测试普通照片
|
||||||
|
testImageType(stage, tempDir, ImageType.NORMAL_PHOTO);
|
||||||
|
|
||||||
|
// 测试拼图
|
||||||
|
testImageType(stage, tempDir, ImageType.PUZZLE);
|
||||||
|
|
||||||
|
// 测试手机上传
|
||||||
|
testImageType(stage, tempDir, ImageType.MOBILE_UPLOAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testImageType(ImageSRStage stage, Path tempDir, ImageType imageType) throws Exception {
|
||||||
|
PhotoProcessContext context = PhotoProcessContext.builder()
|
||||||
|
.processId("test-" + imageType.getCode())
|
||||||
|
.originalUrl("https://example.com/test.jpg")
|
||||||
|
.scenicId(123L)
|
||||||
|
.imageType(imageType)
|
||||||
|
.source(ImageSource.IPC)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
File testFile = tempDir.resolve("test-" + imageType.getCode() + ".jpg").toFile();
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(testFile)) {
|
||||||
|
fos.write(new byte[]{1, 2, 3, 4, 5});
|
||||||
|
}
|
||||||
|
context.setOriginalFile(testFile);
|
||||||
|
|
||||||
|
context.loadStageConfig(null, java.util.Map.of("image_sr", true));
|
||||||
|
|
||||||
|
// 应该可以执行(因为 source 是 IPC)
|
||||||
|
assertTrue(stage.shouldExecute(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testConfigValidation_AllTodoFields() {
|
||||||
|
BceEnhancerConfig config = new BceEnhancerConfig();
|
||||||
|
config.setAppId("TODO_APP_ID");
|
||||||
|
config.setApiKey("TODO_API_KEY");
|
||||||
|
config.setSecretKey("TODO_SECRET_KEY");
|
||||||
|
|
||||||
|
ImageSRStage stage = new ImageSRStage(config);
|
||||||
|
|
||||||
|
PhotoProcessContext context = PhotoProcessContext.builder()
|
||||||
|
.processId("test-all-todo")
|
||||||
|
.originalUrl("https://example.com/test.jpg")
|
||||||
|
.scenicId(123L)
|
||||||
|
.source(ImageSource.IPC)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
File tempFile = context.getTempFileManager().createTempFile("test", ".jpg");
|
||||||
|
try (java.io.FileOutputStream fos = new java.io.FileOutputStream(tempFile)) {
|
||||||
|
fos.write(new byte[]{1, 2, 3, 4, 5});
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
context.setOriginalFile(tempFile);
|
||||||
|
|
||||||
|
StageResult result = stage.execute(context);
|
||||||
|
|
||||||
|
assertTrue(result.isSkipped());
|
||||||
|
assertEquals("配置不完整,跳过超分辨率", result.getMessage());
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
context.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testConfigValidation_MixedTodoAndValid() {
|
||||||
|
BceEnhancerConfig config = new BceEnhancerConfig();
|
||||||
|
config.setAppId("valid-app-id");
|
||||||
|
config.setApiKey("TODO_API_KEY");
|
||||||
|
config.setSecretKey("valid-secret-key");
|
||||||
|
|
||||||
|
ImageSRStage stage = new ImageSRStage(config);
|
||||||
|
|
||||||
|
PhotoProcessContext context = PhotoProcessContext.builder()
|
||||||
|
.processId("test-mixed-todo")
|
||||||
|
.originalUrl("https://example.com/test.jpg")
|
||||||
|
.scenicId(123L)
|
||||||
|
.source(ImageSource.IPC)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
File tempFile = context.getTempFileManager().createTempFile("test", ".jpg");
|
||||||
|
try (java.io.FileOutputStream fos = new java.io.FileOutputStream(tempFile)) {
|
||||||
|
fos.write(new byte[]{1, 2, 3, 4, 5});
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
context.setOriginalFile(tempFile);
|
||||||
|
|
||||||
|
StageResult result = stage.execute(context);
|
||||||
|
|
||||||
|
assertTrue(result.isSkipped());
|
||||||
|
assertEquals("配置不完整,跳过超分辨率", result.getMessage());
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
context.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试说明:
|
||||||
|
*
|
||||||
|
* 由于 ImageSRStage 依赖外部 API (BceImageSR),
|
||||||
|
* 以下测试场景需要 Mock 或集成测试环境:
|
||||||
|
*
|
||||||
|
* 1. testExecute_Success_ValidConfig()
|
||||||
|
* - 模拟成功的图像超分辨率调用
|
||||||
|
* - 验证超分后文件大小变化
|
||||||
|
* - 验证 context.updateProcessedFile() 被调用
|
||||||
|
*
|
||||||
|
* 2. testExecute_ApiReturnsNull_ShouldDegrade()
|
||||||
|
* - 模拟 API 返回 null
|
||||||
|
* - 验证返回 StageResult.degraded()
|
||||||
|
*
|
||||||
|
* 3. testExecute_ApiReturnsEmptyFile_ShouldDegrade()
|
||||||
|
* - 模拟 API 返回空文件
|
||||||
|
* - 验证返回 StageResult.degraded()
|
||||||
|
*
|
||||||
|
* 4. testExecute_ApiThrowsException_ShouldDegrade()
|
||||||
|
* - 模拟 API 抛出异常
|
||||||
|
* - 验证返回 StageResult.degraded() 而不是 failed()
|
||||||
|
*
|
||||||
|
* 5. testExecute_SaveFileFails_ShouldDegrade()
|
||||||
|
* - 模拟文件保存失败
|
||||||
|
* - 验证返回 StageResult.degraded()
|
||||||
|
*
|
||||||
|
* 这些测试需要使用 Mockito 或类似的 Mock 框架来模拟 BceImageSR 的行为。
|
||||||
|
*
|
||||||
|
* 示例(使用 Mockito):
|
||||||
|
*
|
||||||
|
* @Test
|
||||||
|
* void testExecute_Success_ValidConfig() {
|
||||||
|
* // 创建 Mock 的 BceImageSR
|
||||||
|
* BceImageSR mockSR = mock(BceImageSR.class);
|
||||||
|
*
|
||||||
|
* // 创建 Mock 的 MultipartFile
|
||||||
|
* MultipartFile mockEnhancedImage = mock(MultipartFile.class);
|
||||||
|
* when(mockEnhancedImage.isEmpty()).thenReturn(false);
|
||||||
|
* when(mockEnhancedImage.getBytes()).thenReturn(new byte[]{1, 2, 3, 4, 5, 6, 7, 8});
|
||||||
|
*
|
||||||
|
* // 配置 Mock 行为
|
||||||
|
* when(mockSR.enhance(anyString())).thenReturn(mockEnhancedImage);
|
||||||
|
*
|
||||||
|
* // 测试逻辑...
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user