Untitled
unknown
plain_text
6 months ago
13 kB
3
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