Skip to content

Commit

Permalink
packageable with pkg, add json output for palette info, such as which…
Browse files Browse the repository at this point in the history
… colors and how much they occur
  • Loading branch information
drake7707 committed Jul 13, 2019
1 parent 1aa53d0 commit e3b7482
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 14 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
*.js
*.js
*.map
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ Generate paint by number images (vectorized with SVG) from any input image.

Try it out [here](https://drake7707.github.io/paintbynumbersgenerator/index.html)

### CLI Version

The CLI version is a self contained node application that does the conversion from arguments, for example:
```
paint-by-numbers-generator-win.exe -i input.png -o output.svg
```
You can change the settings in settings.json or optionally specify a specific settings.json with the `-c path_to_settings.json` argument.


## Screenshots

![Screenshot](https://i.imgur.com/6uHm78x.png])
Expand All @@ -23,3 +32,8 @@ Try it out [here](https://drake7707.github.io/paintbynumbersgenerator/index.html
I used VSCode, which has built in typescript support. To debug it uses a tiny webserver to host the files on localhost.

To run do `npm install` to restore packages and then `npm start` to start the webserver


## Compiling the cli version

Install pkg first if you don't have it yet `npm install pkg -g`. Then in the root folder run `pkg .`. This will generate the output for linux, windows and macos.
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "paint-by-numbers-generator",
"version": "1.0.0",
"description": "Paint by numbers generator",
"bin": "./src-cli/main.js",
"scripts": {
"lite": "lite-server --port 10001",
"start": "npm run lite"
Expand Down
37 changes: 35 additions & 2 deletions src-cli/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as canvas from "canvas";
import * as minimist from "minimist";
import * as process from "process";

import * as path from "path";
import * as fs from "fs";
import { ColorReducer } from "../src/colorreductionmanagement";
import { RGB } from "../src/common";
Expand All @@ -22,9 +22,18 @@ async function main() {
const imagePath = args["i"];
const svgPath = args["o"];

if (typeof imagePath === "undefined" || typeof svgPath === "undefined") {
console.log("Usage: exe -i <input_image> -o <output_svg> [-c <settings_json>]");
process.exit(1);
}

let configPath = args["c"];
if (typeof configPath === "undefined") {
configPath = "./settings.json";
configPath = path.join(process.cwd(), "settings.json");
} else {
if (!path.isAbsolute(configPath)) {
configPath = path.join(process.cwd(), configPath);
}
}

const settings: CLISettings = require(configPath);
Expand Down Expand Up @@ -107,6 +116,30 @@ async function main() {
const svgString = await createSVG(facetResult, colormapResult.colorsByIndex, settings.svgSizeMultiplier, settings.svgFillFacets, settings.svgShowBorders, settings.svgShowLabels, settings.svgFontSize);

fs.writeFileSync(svgPath, svgString);

console.log("Generating palette info");
const palettePath = path.join(path.dirname(svgPath), path.basename(svgPath).substr(0, path.basename(svgPath).length - path.extname(svgPath).length) + ".json");

const colorFrequency: number[] = [];
for (const color of colormapResult.colorsByIndex) {
colorFrequency.push(0);
}

for (const facet of facetResult.facets) {
if (facet !== null) {
colorFrequency[facet.color] += facet.pointCount;
}
}

const paletteInfo = JSON.stringify(colormapResult.colorsByIndex.map((color, index) => {
return {
index: index,
color: color,
frequency: colorFrequency[index]
};
}), null, 2);

fs.writeFileSync(palettePath, paletteInfo);
}

async function createSVG(facetResult: FacetResult, colorsByIndex: RGB[], sizeMultiplier: number, fill: boolean, stroke: boolean, addColorLabels: boolean, fontSize: number = 6, onUpdate: ((progress: number) => void) | null = null) {
Expand Down
13 changes: 13 additions & 0 deletions src-cli/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,22 @@
"kMeansNrOfClusters": 16,
"kMeansMinDeltaDifference": 1,
"kMeansClusteringColorSpace": 0,

"kMeansColorRestrictions": [
[ 0, 0 ,0 ],
[ 255, 0, 0 ],
[ 0, 255, 0 ],
[ 0, 0, 255 ],
[ 64, 64, 64 ],
[ 128, 128, 128 ],
[ 192, 192, 192 ],
[ 255, 255, 255 ]
],

"removeFacetsSmallerThanNrOfPoints": 20,
"removeFacetsFromLargeToSmall": true,
"nrOfTimesToHalveBorderSegments": 2,

"resizeImageIfTooLarge": true,
"resizeImageWidth": 1024,
"resizeImageHeight": 1024,
Expand Down
31 changes: 28 additions & 3 deletions src/colorreductionmanagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,15 @@ export class ColorReducer {

await delay(0);
if (onUpdate != null) {
ColorReducer.updateKmeansOutputImageData(kmeans, settings, pointsByColor, imgData, outputImgData);
ColorReducer.updateKmeansOutputImageData(kmeans, settings, pointsByColor, imgData, outputImgData, false);
onUpdate(kmeans);
}
}

}

// update the output image data (because it will be used for further processing)
ColorReducer.updateKmeansOutputImageData(kmeans, settings, pointsByColor, imgData, outputImgData);
ColorReducer.updateKmeansOutputImageData(kmeans, settings, pointsByColor, imgData, outputImgData, true);

if (onUpdate != null) {
onUpdate(kmeans);
Expand All @@ -140,7 +140,8 @@ export class ColorReducer {
/**
* Updates the image data from the current kmeans centroids and their respective associated colors (vectors)
*/
public static updateKmeansOutputImageData(kmeans: KMeans, settings: Settings, pointsByColor: IMap<number[]>, imgData: ImageData, outputImgData: ImageData) {
public static updateKmeansOutputImageData(kmeans: KMeans, settings: Settings, pointsByColor: IMap<number[]>, imgData: ImageData, outputImgData: ImageData, restrictToSpecifiedColors: boolean) {


for (let c: number = 0; c < kmeans.centroids.length; c++) {
// for each cluster centroid
Expand All @@ -163,6 +164,30 @@ export class ColorReducer {
rgb = centroid.values;
}

if (restrictToSpecifiedColors) {
if (settings.kMeansColorRestrictions.length > 0) {
// there are color restrictions, for each centroid find the color from the color restrictions that's the closest
let minDistance = Number.MAX_VALUE;
let closestRestrictedColor: RGB | null = null;
for (const color of settings.kMeansColorRestrictions) {
// RGB distance is not very good for the human eye perception, convert both to lab and then calculate the distance
const centroidLab = rgb2lab(rgb);
const restrictionLab = rgb2lab(color);
const distance = Math.sqrt((centroidLab[0] - restrictionLab[0]) * (centroidLab[0] - restrictionLab[0]) +
(centroidLab[1] - restrictionLab[1]) * (centroidLab[1] - restrictionLab[1]) +
(centroidLab[2] - restrictionLab[2]) * (centroidLab[2] - restrictionLab[2]));
if (distance < minDistance) {
minDistance = distance;
closestRestrictedColor = color;
}
}
// use this color instead
if (closestRestrictedColor !== null) {
rgb = closestRestrictedColor;
}
}
}

// replace all pixels of the old color by the new centroid color
const pointColor = `${v.values[0]},${v.values[1]},${v.values[2]}`;
for (const pt of pointsByColor[pointColor]) {
Expand Down
4 changes: 4 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RGB } from "./common";

export enum ClusteringColorSpace {
RGB = 0,
Expand All @@ -9,6 +10,9 @@ export class Settings {
public kMeansNrOfClusters: number = 16;
public kMeansMinDeltaDifference: number = 1;
public kMeansClusteringColorSpace: ClusteringColorSpace = ClusteringColorSpace.RGB;

public kMeansColorRestrictions: RGB[] = [];

public removeFacetsSmallerThanNrOfPoints: number = 20;
public removeFacetsFromLargeToSmall: boolean = true;
public nrOfTimesToHalveBorderSegments: number = 2;
Expand Down

0 comments on commit e3b7482

Please sign in to comment.