2025-11-13 16:34:41 +08:00
|
|
|
// server/utils/imagePreprocessor.js
|
|
|
|
|
import sharp from 'sharp';
|
|
|
|
|
|
|
|
|
|
class ImagePreprocessor {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.tempDir = './temp/processed';
|
2025-11-13 18:09:31 +08:00
|
|
|
this.logger = {
|
|
|
|
|
info: (msg, ...args) => console.log(`🖼️ [预处理] ${msg}`, ...args),
|
|
|
|
|
error: (msg, ...args) => console.error(`❌ [预处理] ${msg}`, ...args),
|
|
|
|
|
debug: (msg, ...args) => console.debug(`❌ [预处理] ${msg}`, ...args)
|
|
|
|
|
};
|
2025-11-13 16:34:41 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async preprocessWithPadding(imagePath, config) {
|
2025-11-13 18:09:31 +08:00
|
|
|
const startTime = Date.now();
|
|
|
|
|
this.logger.info(`开始预处理: ${imagePath}`);
|
|
|
|
|
|
2025-11-13 16:34:41 +08:00
|
|
|
try {
|
|
|
|
|
const metadata = await sharp(imagePath).metadata();
|
2025-11-13 18:09:31 +08:00
|
|
|
this.logger.info(`原始尺寸: ${metadata.width}x${metadata.height}`);
|
2025-11-13 16:34:41 +08:00
|
|
|
|
2025-11-13 18:09:31 +08:00
|
|
|
// 智能填充策略
|
|
|
|
|
const { paddingX, paddingY } = this.calculateSmartPadding(metadata);
|
2025-11-13 16:34:41 +08:00
|
|
|
const paddedWidth = metadata.width + paddingX * 2;
|
|
|
|
|
const paddedHeight = metadata.height + paddingY * 2;
|
|
|
|
|
|
2025-11-13 18:09:31 +08:00
|
|
|
this.logger.debug(`添加填充: ${paddingX}x${paddingY}, 新尺寸: ${paddedWidth}x${paddedHeight}`);
|
|
|
|
|
|
2025-11-13 16:34:41 +08:00
|
|
|
const paddedBuffer = await sharp(imagePath)
|
|
|
|
|
.extend({
|
|
|
|
|
top: paddingY,
|
|
|
|
|
bottom: paddingY,
|
|
|
|
|
left: paddingX,
|
|
|
|
|
right: paddingX,
|
|
|
|
|
background: { r: 255, g: 255, b: 255 }
|
|
|
|
|
})
|
|
|
|
|
.png()
|
|
|
|
|
.toBuffer();
|
|
|
|
|
|
|
|
|
|
const { width, height } = this.resizeForDetection({
|
|
|
|
|
width: paddedWidth,
|
|
|
|
|
height: paddedHeight
|
|
|
|
|
}, config);
|
|
|
|
|
|
|
|
|
|
const resizedBuffer = await sharp(paddedBuffer)
|
|
|
|
|
.resize(width, height)
|
|
|
|
|
.png()
|
|
|
|
|
.toBuffer();
|
|
|
|
|
|
2025-11-13 18:09:31 +08:00
|
|
|
const processingTime = Date.now() - startTime;
|
|
|
|
|
this.logger.info(`预处理完成: ${width}x${height}, 耗时${processingTime}ms`);
|
2025-11-13 16:34:41 +08:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
processedImage: {
|
|
|
|
|
buffer: resizedBuffer,
|
2025-11-13 18:09:31 +08:00
|
|
|
width, height,
|
2025-11-13 16:34:41 +08:00
|
|
|
originalWidth: metadata.width,
|
|
|
|
|
originalHeight: metadata.height,
|
2025-11-13 18:09:31 +08:00
|
|
|
paddedWidth, paddedHeight,
|
|
|
|
|
paddingX, paddingY,
|
2025-11-13 16:34:41 +08:00
|
|
|
scaleX: paddedWidth / width,
|
|
|
|
|
scaleY: paddedHeight / height
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-11-13 18:09:31 +08:00
|
|
|
this.logger.error('预处理错误', error);
|
2025-11-13 16:34:41 +08:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-13 18:09:31 +08:00
|
|
|
calculateSmartPadding(metadata) {
|
|
|
|
|
const basePadding = 20;
|
|
|
|
|
const minPadding = 15;
|
|
|
|
|
|
|
|
|
|
// 根据图像尺寸动态调整填充
|
|
|
|
|
const widthRatio = Math.max(0.02, Math.min(0.08, 100 / metadata.width));
|
|
|
|
|
const heightRatio = Math.max(0.02, Math.min(0.08, 100 / metadata.height));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
paddingX: Math.max(minPadding, Math.floor(metadata.width * widthRatio)),
|
|
|
|
|
paddingY: Math.max(minPadding, Math.floor(metadata.height * heightRatio))
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-13 16:34:41 +08:00
|
|
|
resizeForDetection(metadata, config) {
|
|
|
|
|
const { width, height } = metadata;
|
|
|
|
|
const limitSideLen = config.detLimitSideLen || 960;
|
|
|
|
|
|
|
|
|
|
let ratio = 1;
|
|
|
|
|
if (Math.max(width, height) > limitSideLen) {
|
|
|
|
|
ratio = limitSideLen / Math.max(width, height);
|
2025-11-13 18:09:31 +08:00
|
|
|
this.logger.debug(`缩放比例: ${ratio.toFixed(4)}`);
|
2025-11-13 16:34:41 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newWidth = Math.floor(width * ratio);
|
|
|
|
|
const newHeight = Math.floor(height * ratio);
|
|
|
|
|
|
2025-11-13 18:09:31 +08:00
|
|
|
// 确保尺寸是32的倍数
|
|
|
|
|
const finalWidth = Math.max(32, Math.floor(newWidth / 32) * 32);
|
|
|
|
|
const finalHeight = Math.max(32, Math.floor(newHeight / 32) * 32);
|
|
|
|
|
|
|
|
|
|
this.logger.debug(`调整后尺寸: ${finalWidth}x${finalHeight}`);
|
|
|
|
|
return { width: finalWidth, height: finalHeight };
|
2025-11-13 16:34:41 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getImageInfo(imagePath) {
|
|
|
|
|
try {
|
|
|
|
|
const metadata = await sharp(imagePath).metadata();
|
|
|
|
|
return {
|
|
|
|
|
width: metadata.width || 0,
|
|
|
|
|
height: metadata.height || 0,
|
|
|
|
|
format: metadata.format || 'unknown',
|
|
|
|
|
processed: false
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
2025-11-13 18:09:31 +08:00
|
|
|
this.logger.error('获取图像信息失败', error);
|
2025-11-13 16:34:41 +08:00
|
|
|
return {
|
2025-11-13 18:09:31 +08:00
|
|
|
width: 0, height: 0, format: 'unknown', processed: false
|
2025-11-13 16:34:41 +08:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default ImagePreprocessor;
|