mamm / src /lib /imageProcessor.js
nomid2's picture
Upload imageProcessor.js
fb4d4aa verified
const sharp = require('sharp')
/**
* 图片处理工具类
* 专门处理长图检测、智能切割和重叠处理
*/
class ImageProcessor {
constructor() {
// 长图阈值:高度超过宽度*2.5倍(更合理的阈值,确保1080x4000等图片能被识别)
this.LONG_IMAGE_RATIO = 2.5
// 重叠像素范围
this.OVERLAP_MIN = 50
this.OVERLAP_MAX = 100
// 默认重叠像素
this.DEFAULT_OVERLAP = 75
// 智能切割参数 - 针对模型识别效果优化
this.OPTIMAL_SEGMENT_HEIGHT = 2500 // 理想片段高度
this.MAX_SEGMENT_HEIGHT = 3000 // 最大片段高度
this.MIN_SEGMENT_HEIGHT = 2000 // 最小片段高度
this.MAX_SEGMENTS = 4 // 最大片段数量限制,避免API处理问题
}
/**
* 检测是否为长图
* @param {Buffer} imageBuffer - 图片缓冲区
* @returns {Promise<{isLongImage: boolean, width: number, height: number, format: string}>}
*/
async detectLongImage(imageBuffer) {
try {
const metadata = await sharp(imageBuffer).metadata()
const { width, height, format } = metadata
if (!width || !height) {
throw new Error('无法获取图片尺寸信息')
}
const isLongImage = height > width * this.LONG_IMAGE_RATIO
const threshold = width * this.LONG_IMAGE_RATIO
console.log(`[长图检测] 图片尺寸: ${width}x${height} (${format}), 阈值: ${threshold}, 判断: ${isLongImage ? '是长图' : '非长图'}`)
return {
isLongImage,
width,
height,
format,
ratio: height / width,
threshold
}
} catch (error) {
throw new Error(`图片尺寸检测失败: ${error.message}`)
}
}
/**
* 计算最优片段高度
* @param {number} width - 图片宽度
* @param {number} height - 图片高度
* @param {number} overlap - 重叠像素数
* @returns {Object} 包含最优高度和片段数的对象
*/
calculateOptimalSegmentHeight(width, height, overlap = this.DEFAULT_OVERLAP) {
// 如果图片高度小于最大片段高度,直接返回原高度
if (height <= this.MAX_SEGMENT_HEIGHT) {
return {
segmentHeight: height,
segmentCount: 1,
strategy: 'single_segment'
}
}
// 计算理想的片段数量(基于理想高度)
let idealSegmentCount = Math.ceil(height / this.OPTIMAL_SEGMENT_HEIGHT)
// 如果片段数量超过最大限制,强制减少片段数量
if (idealSegmentCount > this.MAX_SEGMENTS) {
idealSegmentCount = this.MAX_SEGMENTS
console.log(`[智能切割] 片段数量超限,强制限制为${this.MAX_SEGMENTS}个片段`)
}
// 计算每个片段的理想高度(不考虑重叠)
const baseSegmentHeight = Math.floor(height / idealSegmentCount)
// 检查是否在合理范围内
if (baseSegmentHeight >= this.MIN_SEGMENT_HEIGHT && baseSegmentHeight <= this.MAX_SEGMENT_HEIGHT) {
return {
segmentHeight: baseSegmentHeight,
segmentCount: idealSegmentCount,
strategy: 'optimal_division'
}
}
// 如果理想高度太小,减少片段数量
if (baseSegmentHeight < this.MIN_SEGMENT_HEIGHT) {
const adjustedSegmentCount = Math.max(1, Math.floor(height / this.MIN_SEGMENT_HEIGHT))
const adjustedSegmentHeight = Math.floor(height / adjustedSegmentCount)
return {
segmentHeight: Math.min(adjustedSegmentHeight, this.MAX_SEGMENT_HEIGHT),
segmentCount: adjustedSegmentCount,
strategy: 'min_height_constraint'
}
}
// 如果理想高度太大,增加片段数量
if (baseSegmentHeight > this.MAX_SEGMENT_HEIGHT) {
const adjustedSegmentCount = Math.ceil(height / this.MAX_SEGMENT_HEIGHT)
const adjustedSegmentHeight = Math.floor(height / adjustedSegmentCount)
return {
segmentHeight: adjustedSegmentHeight,
segmentCount: adjustedSegmentCount,
strategy: 'max_height_constraint'
}
}
// 兜底策略:使用最大高度
return {
segmentHeight: this.MAX_SEGMENT_HEIGHT,
segmentCount: Math.ceil(height / this.MAX_SEGMENT_HEIGHT),
strategy: 'fallback'
}
}
/**
* 计算切割参数(智能自适应版本)
* @param {number} width - 图片宽度
* @param {number} height - 图片高度
* @param {number} overlap - 重叠像素数
* @returns {Array<{top: number, left: number, width: number, height: number}>}
*/
calculateCropRegions(width, height, overlap = this.DEFAULT_OVERLAP) {
const regions = []
// 计算最优片段高度
const optimalConfig = this.calculateOptimalSegmentHeight(width, height, overlap)
const { segmentHeight: baseSegmentHeight, segmentCount, strategy } = optimalConfig
console.log(`[智能切割] 图片尺寸: ${width}x${height}, 策略: ${strategy}, 预计片段数: ${segmentCount}, 基础高度: ${baseSegmentHeight}`)
let currentTop = 0
let segmentIndex = 0
while (currentTop < height && segmentIndex < segmentCount * 2) { // 防止无限循环
const remainingHeight = height - currentTop
let segmentHeight = Math.min(baseSegmentHeight, remainingHeight)
// 如果不是第一个片段,需要向上扩展重叠区域
if (segmentIndex > 0) {
const overlapTop = Math.min(overlap, currentTop)
currentTop = currentTop - overlapTop
segmentHeight = Math.min(baseSegmentHeight + overlapTop, height - currentTop)
}
// 如果不是最后一个片段,需要向下扩展重叠区域
const isLastSegment = (currentTop + segmentHeight >= height) || (segmentIndex >= segmentCount - 1)
if (!isLastSegment) {
const overlapBottom = Math.min(overlap, remainingHeight - segmentHeight)
segmentHeight = Math.min(segmentHeight + overlapBottom, height - currentTop)
}
// 确保片段高度在合理范围内
segmentHeight = Math.max(segmentHeight, this.MIN_SEGMENT_HEIGHT)
segmentHeight = Math.min(segmentHeight, this.MAX_SEGMENT_HEIGHT)
segmentHeight = Math.min(segmentHeight, height - currentTop) // 不能超出图片边界
regions.push({
top: currentTop,
left: 0,
width: width,
height: segmentHeight,
segmentIndex: segmentIndex,
isFirst: segmentIndex === 0,
isLast: currentTop + segmentHeight >= height,
strategy: strategy
})
console.log(`[切割片段] 片段${segmentIndex + 1}: top=${currentTop}, height=${segmentHeight}, 范围=[${currentTop}, ${currentTop + segmentHeight}]`)
// 移动到下一个片段的起始位置
if (segmentIndex === 0) {
// 第一个片段:直接移动基础高度
currentTop += baseSegmentHeight
} else {
// 后续片段:移动基础高度减去重叠
currentTop += Math.max(baseSegmentHeight - overlap, 1)
}
segmentIndex++
// 如果已经覆盖了整个图片,退出循环
if (currentTop >= height) {
break
}
}
console.log(`[切割完成] 实际生成${regions.length}个片段`)
return regions
}
/**
* 根据原图格式选择最优输出格式
* @param {string} originalFormat - 原图格式
* @returns {string} 最优输出格式
*/
getOptimalOutputFormat(originalFormat) {
// 格式映射表:原格式 -> 最优输出格式
const formatMap = {
'jpeg': 'jpeg',
'jpg': 'jpeg',
'png': 'png',
'webp': 'webp',
'gif': 'png', // GIF转PNG保持透明度
'bmp': 'png', // BMP转PNG减小文件大小
'tiff': 'png', // TIFF转PNG兼容性更好
'svg': 'png' // SVG转PNG用于显示
}
return formatMap[originalFormat?.toLowerCase()] || 'png'
}
/**
* 应用最优格式转换
* @param {Object} sharpInstance - Sharp实例
* @param {string} format - 目标格式
* @param {Object} options - 格式选项
* @returns {Object} 配置后的Sharp实例
*/
applyOptimalFormat(sharpInstance, format, options = {}) {
switch (format) {
case 'jpeg':
return sharpInstance.jpeg({
quality: options.quality || 85,
progressive: true
})
case 'png':
return sharpInstance.png({
compressionLevel: options.compressionLevel || 6,
progressive: true
})
case 'webp':
return sharpInstance.webp({
quality: options.quality || 80,
effort: 4
})
default:
return sharpInstance.png({ compressionLevel: 6 })
}
}
/**
* 切割长图为多个片段
* @param {Buffer} imageBuffer - 原始图片缓冲区
* @param {number} overlap - 重叠像素数
* @returns {Promise<Array<{buffer: Buffer, metadata: Object}>>}
*/
async cropLongImage(imageBuffer, overlap = this.DEFAULT_OVERLAP) {
try {
const detection = await this.detectLongImage(imageBuffer)
if (!detection.isLongImage) {
// 不是长图,返回原图
return [{
buffer: imageBuffer,
metadata: {
segmentIndex: 0,
totalSegments: 1,
isFirst: true,
isLast: true,
originalWidth: detection.width,
originalHeight: detection.height,
segmentWidth: detection.width,
segmentHeight: detection.height
}
}]
}
const { width, height, format } = detection
// 计算最优切割配置
const optimalConfig = this.calculateOptimalSegmentHeight(width, height, overlap)
const regions = this.calculateCropRegions(width, height, overlap)
const segments = []
// 选择最优输出格式
const outputFormat = this.getOptimalOutputFormat(format)
console.log(`检测到长图 ${width}x${height} (${format}),智能切割策略: ${optimalConfig.strategy}`)
console.log(`切割配置: 目标高度=${optimalConfig.segmentHeight}px, 预计片段=${optimalConfig.segmentCount}个, 实际片段=${regions.length}个`)
console.log(`输出格式: ${outputFormat}, 重叠像素: ${overlap}px`)
for (const region of regions) {
const sharpInstance = sharp(imageBuffer)
.extract({
left: region.left,
top: region.top,
width: region.width,
height: region.height
})
// 应用最优格式
const segmentBuffer = await this.applyOptimalFormat(sharpInstance, outputFormat)
.toBuffer()
segments.push({
buffer: segmentBuffer,
metadata: {
segmentIndex: region.segmentIndex,
totalSegments: regions.length,
isFirst: region.isFirst,
isLast: region.isLast,
originalWidth: width,
originalHeight: height,
segmentWidth: region.width,
segmentHeight: region.height,
originalFormat: format,
outputFormat: outputFormat,
strategy: region.strategy,
cropRegion: {
top: region.top,
left: region.left,
width: region.width,
height: region.height
}
}
})
const heightStatus = region.height <= 2500 ? '✓理想' : region.height <= 3000 ? '✓良好' : '⚠超限'
console.log(`生成片段 ${region.segmentIndex + 1}/${regions.length}: ${region.width}x${region.height} (${heightStatus}) [${region.top}-${region.top + region.height}]`)
}
return segments
} catch (error) {
throw new Error(`长图切割失败: ${error.message}`)
}
}
/**
* 生成片段文件名
* @param {string} originalName - 原始文件名
* @param {number} segmentIndex - 片段索引
* @param {number} totalSegments - 总片段数
* @returns {string}
*/
generateSegmentName(originalName, segmentIndex, totalSegments) {
const nameWithoutExt = originalName.replace(/\.[^/.]+$/, '')
const ext = originalName.includes('.') ? originalName.split('.').pop() : 'png'
return `${nameWithoutExt}_part${segmentIndex + 1}of${totalSegments}.${ext}`
}
/**
* 获取处理统计信息
* @param {Array} segments - 切割后的片段数组
* @returns {Object}
*/
getProcessingStats(segments) {
if (!segments || segments.length === 0) {
return { totalSegments: 0, isLongImage: false }
}
const firstSegment = segments[0]
const metadata = firstSegment.metadata
return {
totalSegments: segments.length,
isLongImage: segments.length > 1,
originalDimensions: {
width: metadata.originalWidth,
height: metadata.originalHeight
},
segmentDimensions: segments.map(segment => ({
width: segment.metadata.segmentWidth,
height: segment.metadata.segmentHeight,
index: segment.metadata.segmentIndex
}))
}
}
}
module.exports = new ImageProcessor()