Untitled

 avatar
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