@chenlei28188/image-processor-mcp
Version:
MCP Server for intelligent image processing and analysis
242 lines • 9.52 kB
JavaScript
import sharp from 'sharp';
import Vibrant from 'node-vibrant';
import { ImageUtils } from '../utils/ImageUtils.js';
export class ColorExtractor {
logger;
constructor(logger) {
this.logger = logger;
}
async extract(source, options = {}) {
this.logger.info('Starting color extraction', {
algorithm: options.algorithm || 'vibrant',
colorCount: options.colorCount || 5
});
try {
const imageBuffer = await ImageUtils.loadImage(source);
let dominantColors;
let palette = {};
if (options.algorithm === 'thief') {
dominantColors = await this.extractWithColorThief(imageBuffer, options);
}
else {
const vibrantResult = await this.extractWithVibrant(imageBuffer, options);
dominantColors = vibrantResult.dominantColors;
palette = vibrantResult.palette;
}
const statistics = await this.calculateStatistics(imageBuffer, dominantColors);
const colorHarmony = this.analyzeColorHarmony(dominantColors);
const result = {
dominantColors,
palette,
statistics,
colorHarmony
};
this.logger.info('Color extraction completed', {
colorsFound: dominantColors.length,
scheme: colorHarmony.scheme,
temperature: colorHarmony.temperature
});
return result;
}
catch (error) {
this.logger.error('Color extraction failed:', error);
throw new Error(`Color extraction failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async extractWithVibrant(imageBuffer, options) {
const vibrant = new Vibrant(imageBuffer, {
colorCount: options.colorCount || 64,
quality: 5
});
const palette = await vibrant.getPalette();
const paletteResult = {};
const dominantColors = [];
// 提取Vibrant调色板
for (const [name, swatch] of Object.entries(palette)) {
if (swatch) {
const colorInfo = this.createColorInfo(swatch.getHex(), swatch.getRgb(), options);
paletteResult[name.toLowerCase()] = colorInfo;
dominantColors.push(colorInfo);
}
}
// 按人口排序获取主色调
const sortedSwatches = Object.values(palette)
.filter(swatch => swatch !== null)
.sort((a, b) => b.getPopulation() - a.getPopulation())
.slice(0, options.colorCount || 5);
const sortedDominantColors = sortedSwatches.map((swatch) => this.createColorInfo(swatch.getHex(), swatch.getRgb(), options));
return {
dominantColors: sortedDominantColors,
palette: paletteResult
};
}
async extractWithColorThief(imageBuffer, options) {
// 使用Sharp进行颜色分析的简化实现
const image = sharp(imageBuffer);
const { channels } = await image.metadata();
// 缩小图片以提高性能
const smallImage = await image
.resize(50, 50, { fit: 'fill' })
.raw()
.toBuffer();
const colorMap = new Map();
const step = channels || 3;
// 统计颜色频率
for (let i = 0; i < smallImage.length; i += step) {
const r = smallImage[i];
const g = smallImage[i + 1];
const b = smallImage[i + 2];
// 量化颜色以减少噪音
const quantizedR = Math.floor(r / 32) * 32;
const quantizedG = Math.floor(g / 32) * 32;
const quantizedB = Math.floor(b / 32) * 32;
const key = `${quantizedR},${quantizedG},${quantizedB}`;
colorMap.set(key, (colorMap.get(key) || 0) + 1);
}
// 获取最频繁的颜色
const sortedColors = Array.from(colorMap.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, options.colorCount || 5);
return sortedColors.map(([rgbString, count]) => {
const [r, g, b] = rgbString.split(',').map(Number);
const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
return this.createColorInfo(hex, [r, g, b], options);
});
}
createColorInfo(hex, rgb, options) {
const rgbObj = Array.isArray(rgb) ? { r: rgb[0], g: rgb[1], b: rgb[2] } : rgb;
const colorInfo = {
hex,
rgb: rgbObj
};
if (options.includeHsl !== false) {
colorInfo.hsl = this.rgbToHsl(rgbObj.r, rgbObj.g, rgbObj.b);
}
return colorInfo;
}
rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = 0, s = 0;
const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100)
};
}
async calculateStatistics(imageBuffer, dominantColors) {
const image = sharp(imageBuffer);
const stats = await image.stats();
// 计算平均颜色
const avgR = Math.round(stats.channels[0].mean);
const avgG = Math.round(stats.channels[1].mean);
const avgB = Math.round(stats.channels[2].mean);
const averageColor = {
hex: `#${avgR.toString(16).padStart(2, '0')}${avgG.toString(16).padStart(2, '0')}${avgB.toString(16).padStart(2, '0')}`,
rgb: { r: avgR, g: avgG, b: avgB }
};
// 计算亮度
const brightness = Math.round((avgR * 0.299 + avgG * 0.587 + avgB * 0.114) / 255 * 100);
// 计算对比度(基于标准差)
const contrast = Math.round(Math.sqrt(Math.pow(stats.channels[0].stdev, 2) +
Math.pow(stats.channels[1].stdev, 2) +
Math.pow(stats.channels[2].stdev, 2)) / 255 * 100);
// 计算饱和度
const saturation = dominantColors.length > 0
? Math.round(dominantColors.reduce((sum, color) => {
const hsl = this.rgbToHsl(color.rgb.r, color.rgb.g, color.rgb.b);
return sum + hsl.s;
}, 0) / dominantColors.length)
: 0;
return {
totalColors: dominantColors.length,
averageColor,
brightness,
contrast,
saturation
};
}
analyzeColorHarmony(colors) {
if (colors.length === 0) {
return { scheme: 'mixed', temperature: 'neutral' };
}
// 转换为HSL进行分析
const hslColors = colors.map(color => this.rgbToHsl(color.rgb.r, color.rgb.g, color.rgb.b));
// 分析色相分布
const hues = hslColors.map(hsl => hsl.h);
const hueRange = Math.max(...hues) - Math.min(...hues);
let scheme;
if (hueRange < 30) {
scheme = 'monochromatic';
}
else if (hueRange < 60) {
scheme = 'analogous';
}
else if (hues.some(h1 => hues.some(h2 => Math.abs(h1 - h2) > 150 && Math.abs(h1 - h2) < 210))) {
scheme = 'complementary';
}
else if (hues.length >= 3 && this.isTriadic(hues)) {
scheme = 'triadic';
}
else {
scheme = 'mixed';
}
// 分析色温
const warmColors = colors.filter(color => {
const hsl = this.rgbToHsl(color.rgb.r, color.rgb.g, color.rgb.b);
return (hsl.h >= 0 && hsl.h <= 60) || (hsl.h >= 300 && hsl.h <= 360);
});
const coolColors = colors.filter(color => {
const hsl = this.rgbToHsl(color.rgb.r, color.rgb.g, color.rgb.b);
return hsl.h >= 180 && hsl.h <= 240;
});
let temperature;
if (warmColors.length > coolColors.length * 1.5) {
temperature = 'warm';
}
else if (coolColors.length > warmColors.length * 1.5) {
temperature = 'cool';
}
else {
temperature = 'neutral';
}
return { scheme, temperature };
}
isTriadic(hues) {
// 简化的三色调检测
for (let i = 0; i < hues.length; i++) {
for (let j = i + 1; j < hues.length; j++) {
for (let k = j + 1; k < hues.length; k++) {
const diff1 = Math.abs(hues[i] - hues[j]);
const diff2 = Math.abs(hues[j] - hues[k]);
const diff3 = Math.abs(hues[k] - hues[i]);
if (Math.abs(diff1 - 120) < 30 && Math.abs(diff2 - 120) < 30 && Math.abs(diff3 - 120) < 30) {
return true;
}
}
}
}
return false;
}
}
//# sourceMappingURL=ColorExtractor.js.map