Untitled
unknown
plain_text
a year ago
12 kB
4
Indexable
// app.js
const express = require('express');
const { createCanvas, Image } = require('canvas');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const app = express();
const upload = multer({ dest: 'uploads/' });
app.use(express.static('public'));
class PixelArtEngine {
constructor(width = 512, height = 512) {
this.sourceCanvas = createCanvas(width, height);
this.pixelCanvas = createCanvas(width, height);
this.sourceCtx = this.sourceCanvas.getContext('2d');
this.pixelCtx = this.pixelCanvas.getContext('2d');
}
async loadImage(imagePath) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const scale = Math.min(512 / img.width, 512 / img.height);
const width = Math.floor(img.width * scale);
const height = Math.floor(img.height * scale);
const x = Math.floor((512 - width) / 2);
const y = Math.floor((512 - height) / 2);
this.sourceCtx.fillStyle = '#FFFFFF';
this.sourceCtx.fillRect(0, 0, 512, 512);
this.sourceCtx.drawImage(img, x, y, width, height);
resolve();
};
img.onerror = reject;
img.src = imagePath;
});
}
async processImage(options) {
const pixelSize = options.pixelSize || 8;
const colorCount = options.colorCount || 16;
const edgeSharpness = options.edgeSharpness || 0.7;
const detailLevel = options.detailLevel || 0.5;
const contrast = options.contrast || 0.5;
const sourceData = this.sourceCtx.getImageData(0, 0, 512, 512);
const processedColors = await this.processColors(sourceData, colorCount, contrast);
const edges = this.detectEdges(processedColors, edgeSharpness);
const pixelated = this.pixelate(processedColors, edges, pixelSize, detailLevel);
this.pixelCtx.putImageData(pixelated, 0, 0);
return this.pixelCanvas.toBuffer();
}
async processColors(imageData, colorCount, contrast) {
const pixels = [];
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const r = this.adjustContrast(data[i], contrast);
const g = this.adjustContrast(data[i + 1], contrast);
const b = this.adjustContrast(data[i + 2], contrast);
pixels.push([r, g, b]);
}
const palette = await this.kMeans(pixels, colorCount);
const result = new ImageData(imageData.width, imageData.height);
for (let i = 0; i < data.length; i += 4) {
const pixel = [data[i], data[i + 1], data[i + 2]];
const newColor = this.findNearestColor(pixel, palette);
result.data[i] = newColor[0];
result.data[i + 1] = newColor[1];
result.data[i + 2] = newColor[2];
result.data[i + 3] = 255;
}
return result;
}
adjustContrast(value, contrast) {
return Math.min(255, Math.max(0, Math.round(
((value / 255 - 0.5) * (contrast + 1) + 0.5) * 255
)));
}
async kMeans(pixels, k) {
const colorMap = new Map();
pixels.forEach(pixel => {
const key = pixel.join(',');
colorMap.set(key, (colorMap.get(key) || 0) + 1);
});
const uniqueColors = Array.from(colorMap.entries())
.sort((a, b) => b[1] - a[1])
.map(([color]) => color.split(',').map(Number));
const centroids = [uniqueColors[0]];
while (centroids.length < k && uniqueColors.length > centroids.length) {
let maxDistance = -1;
let farthestColor = null;
for (const color of uniqueColors) {
if (centroids.some(c => this.arraysEqual(c, color))) continue;
let minDistance = Math.min(...centroids.map(c => this.colorDistance(color, c)));
const frequency = colorMap.get(color.join(','));
const weightedDistance = minDistance * Math.log1p(frequency);
if (weightedDistance > maxDistance) {
maxDistance = weightedDistance;
farthestColor = color;
}
}
if (farthestColor) centroids.push(farthestColor);
}
let changed = true;
let iterations = 0;
while (changed && iterations < 50) {
changed = false;
const clusters = Array(k).fill().map(() => []);
uniqueColors.forEach(color => {
const frequency = colorMap.get(color.join(','));
const closestCentroidIndex = this.findClosestCentroidIndex(color, centroids);
for (let i = 0; i < frequency; i++) {
clusters[closestCentroidIndex].push(color);
}
});
for (let i = 0; i < k; i++) {
if (clusters[i].length > 0) {
const newCentroid = this.calculateCentroid(clusters[i]);
if (!this.arraysEqual(newCentroid, centroids[i])) {
centroids[i] = newCentroid;
changed = true;
}
}
}
iterations++;
}
return this.optimizePalette(centroids);
}
optimizePalette(colors) {
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;
});
}
pixelate(imageData, edges, pixelSize, detailLevel) {
const result = new ImageData(imageData.width, imageData.height);
const data = imageData.data;
const width = imageData.width;
for (let y = 0; y < imageData.height; y += pixelSize) {
for (let x = 0; x < width; x += pixelSize) {
const blockColors = [];
const edgeColors = [];
for (let py = 0; py < pixelSize && y + py < imageData.height; py++) {
for (let px = 0; px < pixelSize && x + px < width; px++) {
const idx = ((y + py) * width + (x + px)) * 4;
const color = [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.getDominantColor(blockColors);
for (let py = 0; py < pixelSize && y + py < imageData.height; py++) {
for (let px = 0; px < pixelSize && x + px < width; px++) {
const idx = ((y + py) * width + (x + px)) * 4;
result.data[idx] = finalColor[0];
result.data[idx + 1] = finalColor[1];
result.data[idx + 2] = finalColor[2];
result.data[idx + 3] = 255;
}
}
}
}
return result;
}
detectEdges(imageData, threshold) {
const data = imageData.data;
const width = imageData.width;
const height = imageData.height;
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;
}
getMostSignificantColor(edgeColors, allColors, detailLevel) {
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))
];
}
findClosestCentroidIndex(color, centroids) {
let minDist = Infinity;
let index = 0;
centroids.forEach((centroid, i) => {
const dist = this.colorDistance(color, centroid);
if (dist < minDist) {
minDist = dist;
index = i;
}
});
return index;
}
calculateCentroid(cluster) {
const sum = [0, 0, 0];
cluster.forEach(color => {
sum[0] += color[0];
sum[1] += color[1];
sum[2] += color[2];
});
return sum.map(v => Math.round(v / cluster.length));
}
colorDistance(c1, c2) {
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);
}
getDominantColor(colors) {
const 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);
}
}
return dominant;
}
findNearestColor(color, palette) {
return palette.reduce((nearest, current) => {
const currentDist = this.colorDistance(color, current);
const nearestDist = this.colorDistance(color, nearest);
return currentDist < nearestDist ? current : nearest;
}, palette[0]);
}
arraysEqual(a, b) {
return a.length === b.length && a.every((v, i) => v === b[i]);
}
sobelX(x, y) {
return [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]][y][x];
}
sobelY(x, y) {
return [[-1, -2, -1], [0, 0, 0], [1, 2, 1]][y][x];
}
}
app.post('/process-image', upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No image file provided' });
}
const engine = new PixelArtEngine();
await engine.loadImage(req.file.path);
const options = {
pixelSize: parseInt(req.body.pixelSize) || 8,
colorCount: parseInt(req.body.colorCount) || 16,
edgeSharpness: parseFloat(req.body.edgeSharpness) || 0.7,
detailLevel: parseFloat(req.body.detailLevel) || 0.5,
contrast: parseFloat(req.body.contrast) || 0.5
};
const processedImageBuffer = await engine.processImage(options);
fs.unlinkSync(req.file.path);
res.set('Content-Type', 'image/png');
res.send(processedImageBuffer);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Image processing failed' });
}
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Editor is loading...
Leave a Comment