-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Colors in Posterizer output #15
Comments
@tooolbox with node-potrace and posterizer, I’m also trying calculate color images. I dug into the code a bit. If I understand it correctly, the posterizer gets a histogram of the luminances of all the pixels (light intensity as grey), and then calculates the steps based on that. After researching a bit, I found that generating the histogram of an image does not necessarily give you the palette of that image. While a histogram would be pick out more neutral and background colors, a color palette would include bright highlights that the human eye would pick out (based on fuzzy difference and distance). However, it seems like you knew that and used thresholds and color intensity to fix it 👏 My thought is that I would hook into the color stops you generate, for example: // color stops
[
{ value: 196, colorIntensity: 0.3764705882352941 },
{ value: 136, colorIntensity: 0.5333333333333333 },
{ value: 99, colorIntensity: 0.6352941176470588 },
{ value: 51, colorIntensity: 0.9176470588235294 }
] And then use those to get the matching colors (unfortunately, I’m not quite sure how to do that 😅 yet) Before I go and figure it out, I’m wondering if this is the direction you would take to create color images. Have you thought about this before? How would you go about making color images? I’d really appreciate your insight and any tips you have 🙏 |
I think that is a wonderful output. Maybe the results based on real pictures aren't great, but if the original image were a drawing or a vector image the outcome would be great. |
Hey @jlarmstrongiv thanks for playing around with it. Honestly I just modified the potrace code enough to make it available to Node, so I'm not that deeply aware of the algorithm and its behavior. I don't particularly object to this package having the capability to output color, but I don't have time to dive in to do that right. I would be open to a PR that does get it right. |
Here’s some old code I was experimenting with (and a more polished version): Codeblock
This code block is licensed under GPL-2.0. const potrace = require("potrace");
const fs = require("fs-extra");
const sharp = require("sharp");
const tinycolor = require("tinycolor2");
const quantize = require("quantize");
const SVGO = require("svgo");
const svgo = new SVGO();
const NearestColor = require("nearest-color");
const replaceAll = require("string.prototype.replaceall");
replaceAll.shim();
// https://stackoverflow.com/a/39077686
const hexToRgb = (hex) =>
hex
.replace(
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
(m, r, g, b) => "#" + r + r + g + g + b + b
)
.substring(1)
.match(/.{2}/g)
.map((x) => parseInt(x, 16));
// https://stackoverflow.com/a/35663683
function hexify(color) {
var values = color
.replace(/rgba?\(/, "")
.replace(/\)/, "")
.replace(/[\s+]/g, "")
.split(",");
var a = parseFloat(values[3] || 1),
r = Math.floor(a * parseInt(values[0]) + (1 - a) * 255),
g = Math.floor(a * parseInt(values[1]) + (1 - a) * 255),
b = Math.floor(a * parseInt(values[2]) + (1 - a) * 255);
return (
"#" +
("0" + r.toString(16)).slice(-2) +
("0" + g.toString(16)).slice(-2) +
("0" + b.toString(16)).slice(-2)
);
}
// https://graphicdesign.stackexchange.com/a/91018
function combineOpacity(a, b) {
return 1 - (1 - a) * (1 - b);
}
function getSolid(svg) {
svg = svg.replaceAll(`fill="black"`, "");
const opacityRegex = /fill-opacity="[\d\.]+"/gi;
const numberRegex = /[\d\.]+/;
const matches = svg.match(opacityRegex);
const colors = Array.from(new Set(matches))
.map((fillOpacity) => ({
fillOpacity,
opacity: Number(fillOpacity.match(numberRegex)[0]),
}))
.sort((a, b) => b.opacity - a.opacity)
.map(({ fillOpacity, opacity }, index, array) => {
// combine all lighter opacities into dark opacity
const lighterColors = array.slice(index);
const trueOpacity = lighterColors.reduce(
(acc, cur) => combineOpacity(acc, cur.opacity),
0
);
// turn opacity into hex
const hex = hexify(`rgba(0, 0, 0, ${trueOpacity})`);
return {
trueOpacity,
fillOpacity,
opacity,
hex,
};
});
for (const color of colors) {
svg = svg.replaceAll(color.fillOpacity, `fill="${color.hex}"`);
}
return svg;
}
async function getPixels(input) {
const image = sharp(input);
const metadata = await image.metadata();
const raw = await image.raw().toBuffer();
const pixels = [];
for (let i = 0; i < raw.length; i = i + metadata.channels) {
const pixel = [];
for (let j = 0; j < metadata.channels; j++) {
pixel.push(raw.readUInt8(i + j));
}
pixels.push(pixel);
}
return { pixels, ...metadata };
}
async function replaceColors(svg, original) {
// if greyscale image, return greyscale svg
if ((await (await sharp(original).metadata()).channels) === 1) {
return svg;
}
const hexRegex = /#([a-f0-9]{3}){1,2}\b/gi;
const matches = svg.match(hexRegex);
const colors = Array.from(new Set(matches));
const pixelIndexesOfNearestColors = {}; // hex: [array of pixel indexes]
colors.forEach((color) => (pixelIndexesOfNearestColors[color] = []));
const svgPixels = await getPixels(Buffer.from(svg));
const nearestColor = NearestColor.from(colors);
svgPixels.pixels.forEach((pixel, index) => {
// curly braces for scope https://stackoverflow.com/a/49350263
switch (svgPixels.channels) {
case 3: {
const [r, g, b] = pixel;
const rgb = `rgb(${r}, ${g}, ${b})`;
const hex = hexify(rgb);
pixelIndexesOfNearestColors[nearestColor(hex)].push(index);
break;
}
case 4: {
const [r, g, b, a] = pixel;
const rgba = `rgba(${r}, ${g}, ${b}, ${a / 255})`;
const hex = hexify(rgba);
pixelIndexesOfNearestColors[nearestColor(hex)].push(index);
break;
}
default:
throw new Error("Unsupported number of channels");
}
});
const originalPixels = await getPixels(original);
const pixelsOfNearestColors = pixelIndexesOfNearestColors;
Object.keys(pixelsOfNearestColors).forEach((hexKey) => {
pixelsOfNearestColors[hexKey] = pixelsOfNearestColors[hexKey].map(
(pixelIndex) => {
const pixel = originalPixels.pixels[pixelIndex];
switch (originalPixels.channels) {
case 3: {
const [r, g, b] = pixel;
const rgb = `rgb(${r}, ${g}, ${b})`;
return hexify(rgb);
}
case 4: {
const [r, g, b, a] = pixel;
const rgba = `rgba(${r}, ${g}, ${b}, ${a / 255})`;
return hexify(rgba);
}
default:
throw new Error("Unsupported number of channels");
}
}
);
});
const colorsToReplace = pixelsOfNearestColors;
// get palette of 5 https://github.com/lokesh/color-thief/blob/master/src/color-thief-node.js#L61
Object.keys(pixelsOfNearestColors).forEach((hexKey) => {
const pixelArray = colorsToReplace[hexKey].map(hexToRgb);
const colorMap = quantize(pixelArray, 5);
const [r, g, b] = colorMap.palette()[0];
const rgb = `rgb(${r}, ${g}, ${b})`;
colorsToReplace[hexKey] = hexify(rgb);
});
Object.entries(colorsToReplace).forEach(([oldColor, newColor]) => {
svg = svg.replaceAll(oldColor, newColor);
});
return svg;
}
async function start() {
let svg = await new Promise((resolve, reject) => {
potrace.posterize(
"./demo.png",
{
// number of colors
// steps: 10,
},
function (err, svg) {
if (err) return reject(err);
resolve(svg);
}
);
});
svg = getSolid(svg);
svg = await replaceColors(svg, await fs.readFile("./demo.png"));
svg = (await svgo.optimize(svg)).data;
fs.outputFileSync("./demo.svg", svg);
sharp(Buffer.from(svg)).png({ colors: 4 }).toFile("./demo-from-svg.png");
}
start();
/**
* One option is turning the image into pixels
* Calculating the luminosity of each pixel
* Grouping it to the nearest step
* And using the color thief https://github.com/lokesh/color-thief/blob/master/src/color-thief-node.js#L82
* To figure out which color is most common for that particular step
*
* Another option is to take the output svg
* Turn it into a png
* Unfortunately, due to a lack of crisp edges / anti aliasing
* You still have to group the colors into the nearest step
* Then you essentially "match" those colors to the coordinates in the color image
* And run the same color thief for that particular step
*/ I think my version is a bit hacky, and there are better ways to implement it. Personally, I think it’s more fitting as a proof of concept, than a PR. While I do not have time to clean up and merge it, a future version should:
The demo is currently broken, because I can’t afford $20/mo 😅 |
Sorry, I don't know if the service was designed only to return grayscale or I if I am doing something wrong.
I tried different options, but still, the output from colourful png is always grayscale.
The text was updated successfully, but these errors were encountered: