Untitled
unknown
plain_text
a year ago
13 kB
6
Indexable
import sharp, { Sharp } from 'sharp';
// Konfiguriere Sharp global
sharp.cache(false);
sharp.concurrency(1);
export interface ProcessOptions {
pixelSize?: number;
colorCount?: number;
edgeSharpness?: number;
detailLevel?: number;
contrast?: number;
}
type RGB = [number, number, number];
type ColorMap = Map<string, number>;
class ProcessImage {
private readonly width: number = 512;
private readonly height: number = 512;
private readonly maxWidth: number = 2048;
private readonly maxHeight: number = 2048;
public async process(
inputPath: string,
options: ProcessOptions
): Promise<Sharp> {
try {
const {
pixelSize = 8,
colorCount = 16,
edgeSharpness = 0.7,
detailLevel = 0.5,
contrast = 0.5
} = options;
// Prüfe und optimiere Bildgröße
const metadata = await sharp(inputPath).metadata();
const resizeOptions = {
width: Math.min(metadata.width || 0, this.maxWidth),
height: Math.min(metadata.height || 0, this.maxHeight),
fit: sharp.fit.inside,
withoutEnlargement: true
};
// Progressives Resizing und Bildoptimierung
let image: Sharp;
if (metadata.width && metadata.height && (metadata.width > 2000 || metadata.height > 2000)) {
image = sharp(inputPath)
.resize(resizeOptions)
.resize(this.width, this.height, {
fit: 'contain',
background: { r: 255, g: 255, b: 255, alpha: 1 }
});
} else {
image = sharp(inputPath)
.resize(this.width, this.height, {
fit: 'contain',
background: { r: 255, g: 255, b: 255, alpha: 1 }
});
}
// Adjust contrast using linear transform
image = image.linear(
1 + contrast,
-(contrast * 128)
);
// Get raw pixel data with error handling
let { data, info } = await image
.raw()
.toBuffer({ resolveWithObject: true })
.catch(error => {
console.error('Error getting raw pixel data:', error);
throw error;
});
// Process colors and get palette
const pixels: RGB[] = this.getRGBPixels(data);
const palette: RGB[] = await this.kMeans(pixels, colorCount);
// Detect edges
const edges: Uint8Array = this.detectEdges(data, info.width, info.height, edgeSharpness);
// Pixelate and apply colors
const processedData: Buffer = this.pixelate(
data,
edges,
info.width,
info.height,
pixelSize,
detailLevel,
palette
);
// Create new sharp image from processed data
return sharp(processedData, {
raw: {
width: info.width,
height: info.height,
channels: 4
}
});
} catch (error) {
if (error.message.includes('memory area too small')) {
console.warn('Memory optimization fallback activated');
// Fallback mit reduzierter Qualität
return sharp(inputPath)
.resize(this.width, this.height, {
fit: 'contain',
background: { r: 255, g: 255, b: 255, alpha: 1 },
withoutEnlargement: true
})
.jpeg({ quality: 80 });
}
throw error;
}
}
private getRGBPixels(data: Buffer): RGB[] {
const pixels: RGB[] = [];
const chunkSize = 1000000; // Verarbeite Daten in Chunks
for (let i = 0; i < data.length; i += chunkSize * 4) {
const end = Math.min(i + chunkSize * 4, data.length);
for (let j = i; j < end; j += 4) {
pixels.push([data[j], data[j + 1], data[j + 2]]);
}
}
return pixels;
}
private async kMeans(pixels: RGB[], k: number): Promise<RGB[]> {
const colorMap: ColorMap = new Map();
// Optimierte Farbverarbeitung in Chunks
const chunkSize = 10000;
for (let i = 0; i < pixels.length; i += chunkSize) {
const chunk = pixels.slice(i, i + chunkSize);
chunk.forEach(pixel => {
const key = pixel.join(',');
colorMap.set(key, (colorMap.get(key) || 0) + 1);
});
}
const uniqueColors: RGB[] = Array.from(colorMap.entries())
.sort((a, b) => b[1] - a[1])
.map(([color]) => color.split(',').map(Number) as RGB);
// Initialize centroids
const centroids: RGB[] = [uniqueColors[0]];
while (centroids.length < k && uniqueColors.length > centroids.length) {
let maxDistance = -1;
let farthestColor: RGB | null = null;
for (const color of uniqueColors) {
if (centroids.some(c => this.arraysEqual(c, color))) continue;
const minDistance = Math.min(...centroids.map(c => this.colorDistance(color, c)));
const frequency = colorMap.get(color.join(',')) || 0;
const weightedDistance = minDistance * Math.log1p(frequency);
if (weightedDistance > maxDistance) {
maxDistance = weightedDistance;
farthestColor = color;
}
}
if (farthestColor) centroids.push(farthestColor);
}
return this.optimizePalette(centroids);
}
private detectEdges(
data: Buffer,
width: number,
height: number,
threshold: number
): Uint8Array {
const edges = new Uint8Array(width * height);
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let gx = 0, gy = 0;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const idx = ((y + ky) * width + (x + kx)) * 4;
const val = (data[idx] + data[idx + 1] + data[idx + 2]) / 3;
gx += val * this.sobelX(kx + 1, ky + 1);
gy += val * this.sobelY(kx + 1, ky + 1);
}
}
const magnitude = Math.sqrt(gx * gx + gy * gy) * threshold;
edges[y * width + x] = magnitude > 128 ? 255 : 0;
}
}
return edges;
}
private pixelate(
data: Buffer,
edges: Uint8Array,
width: number,
height: number,
pixelSize: number,
detailLevel: number,
palette: RGB[]
): Buffer {
const result = Buffer.alloc(data.length);
for (let y = 0; y < height; y += pixelSize) {
for (let x = 0; x < width; x += pixelSize) {
const blockColors: RGB[] = [];
const edgeColors: RGB[] = [];
for (let py = 0; py < pixelSize && y + py < height; py++) {
for (let px = 0; px < pixelSize && x + px < width; px++) {
const idx = ((y + py) * width + (x + px)) * 4;
const color: RGB = [data[idx], data[idx + 1], data[idx + 2]];
if (edges[(y + py) * width + (x + px)] > 128) {
edgeColors.push(color);
}
blockColors.push(color);
}
}
const finalColor = edgeColors.length > 0
? this.getMostSignificantColor(edgeColors, blockColors, detailLevel)
: this.findNearestColor(this.getDominantColor(blockColors), palette);
for (let py = 0; py < pixelSize && y + py < height; py++) {
for (let px = 0; px < pixelSize && x + px < width; px++) {
const idx = ((y + py) * width + (x + px)) * 4;
result[idx] = finalColor[0];
result[idx + 1] = finalColor[1];
result[idx + 2] = finalColor[2];
result[idx + 3] = 255;
}
}
}
}
return result;
}
private colorDistance(c1: RGB, c2: RGB): number {
const rmean = (c1[0] + c2[0]) / 2;
const r = c1[0] - c2[0];
const g = c1[1] - c2[1];
const b = c1[2] - c2[2];
return Math.sqrt((2 + rmean/256) * r*r + 4 * g*g + (2 + (255-rmean)/256) * b*b);
}
private getDominantColor(colors: RGB[]): RGB {
const colorMap: ColorMap = new Map();
colors.forEach(color => {
const key = color.join(',');
colorMap.set(key, (colorMap.get(key) || 0) + 1);
});
let maxCount = 0;
let dominant = colors[0];
for (const [key, count] of colorMap.entries()) {
if (count > maxCount) {
maxCount = count;
dominant = key.split(',').map(Number) as RGB;
}
}
return dominant;
}
private getMostSignificantColor(
edgeColors: RGB[],
allColors: RGB[],
detailLevel: number
): RGB {
const edgeColor = this.getDominantColor(edgeColors);
const dominantColor = this.getDominantColor(allColors);
return [
Math.round(edgeColor[0] * detailLevel + dominantColor[0] * (1 - detailLevel)),
Math.round(edgeColor[1] * detailLevel + dominantColor[1] * (1 - detailLevel)),
Math.round(edgeColor[2] * detailLevel + dominantColor[2] * (1 - detailLevel))
];
}
private findNearestColor(color: RGB, palette: RGB[]): RGB {
return palette.reduce((nearest, current) => {
const currentDist = this.colorDistance(color, current);
const nearestDist = this.colorDistance(color, nearest);
return currentDist < nearestDist ? current : nearest;
}, palette[0]);
}
private optimizePalette(colors: RGB[]): RGB[] {
const hasBlack = colors.some(c => c.every(v => v < 32));
const hasWhite = colors.some(c => c.every(v => v > 223));
if (!hasBlack) colors[colors.length - 1] = [0, 0, 0];
if (!hasWhite) colors[colors.length - 2] = [255, 255, 255];
return colors.sort((a, b) => {
const lumA = (a[0] * 299 + a[1] * 587 + a[2] * 114) / 1000;
const lumB = (b[0] * 299 + b[1] * 587 + b[2] * 114) / 1000;
return lumA - lumB;
});
}
private arraysEqual(a: RGB, b: RGB): boolean {
return a.length === b.length && a.every((v, i) => v === b[i]);
}
private sobelX(x: number, y: number): number {
return [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]][y][x];
}
private sobelY(x: number, y: number): number {
return [[-1, -2, -1], [0, 0, 0], [1, 2, 1]][y][x];
}
}
export const imageProcessor = new ProcessImage();
Die wichtigsten Änderungen und Optimierungen im Überblick:
1. Globale Sharp-Konfiguration für besseres Speichermanagement
2. Maximale Bildgrößenbeschränkung
3. Progressives Resizing für große Bilder
4. Chunk-basierte Verarbeitung für große Datenmengen
5. Verbesserte Fehlerbehandlung mit Fallback-Option
6. Speicheroptimierte Verarbeitung in getRGBPixels und kMeans
7. Try-Catch-Blöcke für bessere Fehlerbehandlung
Um den Code zu verwenden, stelle sicher, dass genügend Arbeitsspeicher zur Verfügung steht. Bei Node.js kann dies über den Parameter --max-old-space-size erfolgen:
node --max-old-space-size=4096 your-script.js
Verwendung des Codes:
const result = await imageProcessor.process('input-image.jpg', {
pixelSize: 8,
colorCount: 16,
edgeSharpness: 0.7,
detailLevel: 0.5,
contrast: 0.5
});
await result.toFile('output-image.jpg');
Diese Version sollte deutlich stabiler laufen und besser mit großen Bildern umgehen können.Editor is loading...
Leave a Comment