MS Paint is underrated
JavaScript
This solution uses the HTML5 canvas element to extract the image data, but without the need to use HTML, that means it can be run in your console. It access the color palette image as an array; I stored all the colors from the palette image in an array). It outputs to the console (after it finishes) and also stores the result in a variable.
The most updated version of the code is in the fiddle. The fiddle also uses a better algorithm to reduce noise in the pictures. The improvement in the algorithm is mostly fixing a function (max to min) which caused the inverse color to be chosen.
Code in the shape of the MS Paint Icon! (formatted code in fiddle or Stack Snippet)
eval(` function
Paint(t){fun
ction n(t){va
r n=t.toString(
16);return 1==n.
length?"0"+n:n}fu
nction e(t){return
"#"+n(t[0])+n(t[1]
)+n(t[2])}var a=ne
w Image,i=document.
createElement("canv
as"),h=null,o=docum
ent.createElement(
"canvas"),r= o.getContext("2d
") ,l=[],u=this,c =[[0,0,0],[255
,2 55,255],[ 192,192, 192],[128,12
8 ,128],[126,3,8],[252,13,27] ,[255,25
3, 56],[128,127,23],[15,127,18],[ 41,253
, 46],[45,255,254],[17,128,127],[2 ,12,1
2 6],[ 11,36,2 51],[252,40,252],[12 7,15,1
2 6],[ 128,127 ,68],[255,253,136],[4 2,253,
1 33], [4,64,64],[23 ,131,251],[133,255,254],
[ 129 ,132,252],[6,6 6,126],[127,37,2 51],[127,
6 4,1 3],[253,128,73],[252,22,129]];a.crossOrigin
= "", a.src=t,this.done=this.done||function(){},a.o
n load=function(){function t(t){var n=0,e=0,a=0;return
t .forEach(function(t){n+=t[0],e+=t[1],a+=t[2]}),[n/t.leng
t h,e /t.length,a/t.length]}function n(t){for(var n=[],e=0;e
< t.l ength;e+=1)n.push(t[e]);return n}function g(t,n){retur
n (Ma th.abs(t[0]-n[0])/255+Math.abs(t[1]-n[1])/255+Math.abs(t
[ 2]- n[2])/255)/3}function f(t,n){for(var e=Math.floor(Math.ran
do m()*n.length),a=n[e],i=(g(t,a),1-.8),h=56,o=[];o.length<=h&
&g (t,a)>i;)if(o.push(a),a=n[Math.floor(Math.random()*n.length)]
, o.length==h){var r=o.map(function(n){return g(t,n)});a=o[r.indexO
f(Math.max.apply(Math,r))],o.push(a)}return a}function s(t,n){for(
v ar e=[];t.length>0;)e.push(t.splice(0,n).slice(0,-1));return e}i.w
i dth=a.width,i.height=2*a.height,h=i.getContext("2d"),h.drawImage(a,0
,0,a.width,a.height);for(var d=(function(t){reduce=t.map(function(t){re
turn(t[ 0]+t[1]+t[2])/3})}(c),0),m=0,v=i.width*i.height/4,p=0;v>p;p+=1)d
>2*Mat h.ceil(a.width/2)&&(d=0,m+=1),l.push(f(t(s(n(h.getImageData(2*d,2
*m,4,4).data),4)),c)),d+=1;o.width=i.width,o.height=i.height;for(var d=0
,m=0,v=i.width*i.height/4,p=0;v>p;p+=1)d>2*Math.ceil(a.width/2)&&(d=0,m+=
1),console.log("Filling point ("+d+", "+m+") : "+e(l[p])),r.fillStyle=e(l
[p]),r.fillRect(2*d+1,2*m,2,1) ,r.fillRect(2*d,2*m+1,4,2),r.fillRect(2*d
+1,2*m+3,2,1),d+=1;u.result=o .toDataURL("image/png"),u.resultCanvas
=o,u.imageCanvas=i,u.image=a ,u.done(),console.log(u.result)},a.one
rror=function(t){console.log ("The image failed to load. "+t)}}/*..
............................ ......................................
. .......................... .....................................
............................ ......................................
............................. .......................................
.......................................................................
.......................................................................
.................. ..................................................
................ .................................................
.............. ................................................
............. ................................................
........... .................................................
......... ................................................
....... ................................................
.... ................................................
................................................
...............................................
...............................................
..............................................
.............................................
............................................
..........................................
.......................................
.....................................
.................................
.............................
......................
.....
.....
.....
....
*/`
.replace(/\n/g,''))
Usage:
Paint('DATA URI');
Fiddle.
The fiddle uses crossorigin.me so you don't need to worry about cross-origin-resource-sharing.
I've also updated the fiddle so you can adjust some values to produce the best-looking painting. Some pictures' colors might be off, to avoid this, adjust the accept_rate to adjust the algorithm. A lower number means better gradients, a higher number will result in sharper colors.
Here's the fiddle as a Stack-Snippet (NOT updated, in case the fiddle doesn't work):
/* Options */
var accept_rate = 82, // 0 (low) - 100 (high)
attempts = 16, // Attemps before giving up
edge_multi = 2; // Contrast, 2-4
function Paint(image_url) {
var image = new Image(), canvas = document.createElement('canvas'), context = null, result = document.createElement('canvas'), resultContext = result.getContext('2d'), final_colors = [], self = this, color_choices = [
[0,0,0],
[255,255,255],
[192,192,192],
[128,128,128],
[126,3,8],
[252,13,27],
[255,253,56],
[128,127,23],
[15,127,18],
[41,253,46],
[45,255,254],
[17,128,127],
[2,12,126],
[11,36,251],
[252,40,252],
[127,15,126],
[128,127,68],
[255,253,136],
[42,253,133],
[4,64,64],
[23,131,251],
[133,255,254],
[129,132,252],
[6,66,126],
[127,37,251],
[127,64,13],
[253,128,73],
[252,22,129]
];
image.crossOrigin = "";
image.src = image_url;
this.done = this.done || function () {};
function hex(c) {
var res = c.toString(16);
return res.length == 1 ? "0" + res : res;
}
function colorHex(r) {
return '#' + hex(r[0]) + hex(r[1]) + hex(r[2]);
}
image.onload = function () {
canvas.width = image.width; canvas.height = image.height * 2;
context = canvas.getContext('2d');
context.drawImage(image, 0, 0, image.width, image.height);
function averageColors(colors_ar) {
var av_r = 0,
av_g = 0,
av_b = 0;
colors_ar.forEach(function (color) {
av_r += color[0];
av_g += color[1];
av_b += color[2];
});
return [av_r / colors_ar.length,
av_g / colors_ar.length,
av_b / colors_ar.length];
}
function arrayFrom(ar) {
var newar = [];
for (var i = 0; i < ar.length; i += 1) {
newar.push(ar[i]);
}
return newar;
}
function colorDif(c1,c2) {
// Get's distance between two colors 0.0 - 1.0
return (Math.abs(c1[0] - c2[0]) / 255 +
Math.abs(c1[1] - c2[1]) / 255 +
Math.abs(c1[2] - c2[2]) / 255) / 3;
}
var furthest = (function (cc) {
// Determines furthest color
// Reduces RGB into a "single value"
reduce = cc.map(function(color) {
return ( color[0] + color [1] + color [2] ) / 3;
});
}(color_choices));
function intDif(i1,i2,t) {
return Math.abs(i1 - i2) / t
}
function arrayIs(ar, int,d) {
return intDif(ar[0],int,255) <= d &&
intDif(ar[1],int,255) <= d &&
intDif(ar[2],int,255) <= d
}
function colorLoop(c1,c2) {
var edgeCap = edge_multi * ((accept_rate / 100) / 50), values = c2.map(function (i) {
return colorDif(c1,i);
});
return arrayIs(c1,255,edgeCap)?[255,255,255]:
arrayIs(c1,0,edgeCap) ?[0,0,0]:
c2[values.indexOf(Math.min.apply(Math, values))];
}
function colorFilter(c1, c2) {
// Does the color stuff
var rand = Math.floor( Math.random() * c2.length ), // Random number
color = c2[rand], // Random color
randdif = colorDif(c1, color),
threshhold = 1 - accept_rate / 100, // If the color passes a threshhold
maxTries = attempts, // To avoid infinite looping, 56 is the maximum tries to reach the threshold
tries = [];
// Repeat until max iterations have been reached or color is close enough
while ( tries.length <= maxTries && colorDif( c1, color ) > threshhold ) {
tries.push(color);
color = c2[Math.floor(Math.random() * c2.length)]; // Tries again
if (tries.length == maxTries) {
// Used to hold color and location
var refLayer = tries.map(function(guess) {
return colorDif(c1, guess);
});
color = tries[refLayer.indexOf(Math.min.apply(Math, refLayer))];
tries.push(color);
}
}
var edgeCap = edge_multi * ((accept_rate / 100) / 50), loop = colorLoop(c1, c2);
return arrayIs(c1,255,edgeCap)?[255,255,255]:
arrayIs(c1,0,edgeCap) ?[0,0,0]:
colorDif(c1,color)<accept_rate?color:
loop;
}
function chunk(ar, len) {
var arrays = [];
while (ar.length > 0)
arrays.push(ar.splice(0, len).slice(0, -1));
return arrays;
}
var x = 0, y = 0, total = (canvas.width * canvas.height) / 4;
for (var i = 0; i < total; i += 1) {
if (x > (Math.ceil(image.width / 2) * 2)) {
x = 0;
y += 1;
}
final_colors.push( colorFilter( averageColors( chunk( arrayFrom(context.getImageData(x * 2, y * 2, 4, 4).data), 4 ) ), color_choices) );
x += 1;
}
// Paint Image
result.width = canvas.width;
result.height = canvas.height;
var x = 0, y = 0, total = (canvas.width * canvas.height) / 4;
for (var i = 0; i < total; i += 1) {
if (x > (Math.ceil(image.width / 2) * 2)) {
x = 0;
y += 1;
}
console.log("Filling point (" + x + ", " + y + ") : " + colorHex(final_colors[i]));
resultContext.fillStyle = colorHex(final_colors[i]);
resultContext.fillRect(x*2 + 1, y * 2, 2 , 1); // Top
resultContext.fillRect(x * 2, y * 2 + 1, 4, 2); // Middle
resultContext.fillRect(x * 2 + 1, y * 2 + 3, 2, 1); // Bottom
x += 1;
}
self.result = result.toDataURL("image/png");
self.resultCanvas = result;
self.imageCanvas = canvas;
self.image = image;
self.done();
console.log(self.result);
};
image.onerror = function(error) {
console.log("The image failed to load. " + error);
}
}
// Demo
document.getElementById('go').onclick = function () {
var url = document.getElementById('image').value;
if (!url.indexOf('data:') == 0) {
url = 'http://crossorigin.me/' + url;
}
var example = new Paint(url);
example.done = function () {
document.getElementById('result').src = example.result;
document.getElementById('result').width = example.resultCanvas.width;
document.getElementById('result').height = example.resultCanvas.height;
window.paint = example;
};
};
<!--This might take a while-->
Enter the image data URI or a URL, I've used crossorigin.me so it can perform CORS requests to the image. If you're inputting a URL, be sure to include the http(s)
<input id="image" placeholder="Image URI or URL"><button id="go">Go</button>
<hr/>
You can get the image URI from a website like <a href="http://jpillora.com/base64-encoder/">this one</a>
<hr/>
Result:
<img id="result">
<span id="error"></span><hr/>
Check your console for any errors. After a second, you should see the colors that are being generated / printed getting outputted to the console.
To commemorate New Horizon's flyby of Pluto, I've inputted an image of Pluto:
For the following I've set it to make them resemble the original as close as possible:
I ran this with OS X Yosemite's default wallpaper. After leaving it run for a bit, the results are absolutely stunning. The original file was huge (26 MB) so I resized and compressed it:
The starry night (I've used a higher resolution image for better results)
A picture I found on google:
JavaScript + HTML
Random:
Random Point
Random Aligned:
Subdivides the canvas into 4x4 squares, and chooses a point randomly inside one of the squares. Offsets will move the grid, so you can fill in the little gaps.
Loop:
Creates a grid and Loops through all the points. Offsets moves the grid. Spacing determine the size of each cell. (They will start to overlap)
Color difference:
- RGB
- HSL
- HSV
var draw = document.getElementById("canvas").getContext("2d");
var data = document.getElementById("data").getContext("2d");
colors = [
[0, 0, 0],
[255, 255, 255],
[192, 192, 192],
[128, 128, 128],
[126, 3, 8],
[252, 13, 27],
[255, 253, 56],
[128, 127, 23],
[15, 127, 18],
[41, 253, 46],
[45, 255, 254],
[17, 128, 127],
[2, 12, 126],
[11, 36, 251],
[252, 40, 252],
[127, 15, 126],
[128, 127, 68],
[255, 253, 136],
[42, 253, 133],
[4, 64, 64],
[23, 131, 251],
[133, 255, 254],
[129, 132, 252],
[6, 66, 126],
[127, 37, 251],
[127, 64, 13],
[253, 128, 73],
[252, 22, 129]
];
iteration = 0;
fails = 0;
success = 0;
x = 0;
y = 0;
//Init when the Go! button is pressed
document.getElementById("file").onchange = function (event) {
document.getElementById("img").src = URL.createObjectURL(event.target.files[0]);
filename = document.getElementById("file").value;
/*if (getCookie("orginal") == filename) {
console.log("Loading from Cookie");
reload = true;
document.getElementById("reload").src = getCookie("picture");
}*/
};
/*function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1);
if (c.indexOf(name) == 0) return c.substring(name.length, c.length);
}
return "";
}*/
//Run when the image has been loaded into memory
document.getElementById("img").onload = function () {
document.getElementById("file").disable = "true";
document.getElementById("canvas").hidden = "";
document.getElementById("canvas").height = document.getElementById("img").height;
document.getElementById("data").height = document.getElementById("img").height;
document.getElementById("canvas").width = document.getElementById("img").width;
document.getElementById("data").width = document.getElementById("img").width;
var imgData = draw.createImageData(document.getElementById("img").width, document.getElementById("img").height);
for (var i = 0; i < imgData.data.length; i += 4) {
imgData.data[i + 0] = 0;
imgData.data[i + 1] = 0;
imgData.data[i + 2] = 0;
imgData.data[i + 3] = 255;
}
draw.putImageData(imgData, 0, 0);
data.putImageData(imgData, 0, 0);
if (reload == true) {
draw.drawImage(document.getElementById("reload"), 0, 0);
}
data.drawImage(document.getElementById("img"), 0, 0);
setInterval(function () {
for (var u = 0; u < document.getElementById("selectColor").value; u++) {
doThing();
}
}, 0);
};
//The core function. Every time this function is called, is checks/adds a dot.
function doThing() {
getCoords();
paintBucket();
console.count("Iteration");
if (compare(x, y)) {
draw.putImageData(imgData, x, y);
}
}
function getCoords() {
switch (document.getElementById("selectCord").value) {
case "1":
x = Math.floor(Math.random() * (document.getElementById("img").width + 4));
y = Math.floor(Math.random() * (document.getElementById("img").height + 4));
break;
case "2":
x = Math.floor(Math.random() * ((document.getElementById("img").width + 4) / 4)) * 4;
console.log(x);
x += parseInt(document.getElementById("allignX").value);
console.log(x);
y = Math.floor(Math.random() * ((document.getElementById("img").height + 4) / 4)) * 4;
y += parseInt(document.getElementById("allignY").value);
break;
case "3":
x += parseInt(document.getElementById("loopX").value);
if (x > document.getElementById("img").width + 5) {
x = parseInt(document.getElementById("allignX").value);
y += parseInt(document.getElementById("loopY").value);
}
if (y > document.getElementById("img").height + 5) {
y = parseInt(document.getElementById("allignY").value);
}
}
}
function compare(arg1, arg2) {
var arg3 = arg1 + 4;
var arg4 = arg2 + 4;
imgData2 = data.getImageData(arg1, arg2, 4, 4);
imgData3 = draw.getImageData(arg1, arg2, 4, 4);
N = 0;
O = 0;
i = 4;
addCompare();
addCompare();
i += 4;
for (l = 0; l < 8; l++) {
addCompare();
}
i += 4;
addCompare();
addCompare();
i += 4;
//console.log("New Score: " + N + " Old Score: " + O);
iteration++;
/*if(iteration>=1000){
document.cookie="orginal="+filename;
document.cookie="picture length="+document.getElementById("canvas").toDataURL().length;
document.cookie="picture="+document.getElementById("canvas").toDataURL();
}*/
if (N < O) {
return true;
} else {
return false;
}
}
function addCompare() {
if (document.getElementById("colorDif").value == "HSL") {
HSLCompare();
i += 4;
return;
}
if (document.getElementById("colorDif").value == "HSV") {
HSVCompare();
i += 4;
return;
}
N += Math.abs(imgData.data[i] - imgData2.data[i]);
N += Math.abs(imgData.data[i + 1] - imgData2.data[i + 1]);
N += Math.abs(imgData.data[i + 2] - imgData2.data[i + 2]);
O += Math.abs(imgData3.data[i] - imgData2.data[i]);
O += Math.abs(imgData3.data[i + 1] - imgData2.data[i + 1]);
O += Math.abs(imgData3.data[i + 2] - imgData2.data[i + 2]);
i += 4;
}
function HSVCompare() {
var NewHue = rgbToHsv(imgData.data[i], imgData.data[i + 1], imgData.data[i + 2])[0];
var PicHue = rgbToHsv(imgData2.data[i], imgData2.data[i + 1], imgData2.data[i + 2])[0];
var OldHue = rgbToHsv(imgData3.data[i], imgData3.data[i + 1], imgData3.data[i + 2])[0];
var NScore = [Math.abs(NewHue - PicHue), ((NewHue < PicHue) ? NewHue + (1 - PicHue) : PicHue + (1 - NewHue))];
var OScore = [Math.abs(OldHue - PicHue), ((OldHue < PicHue) ? OldHue + (1 - PicHue) : PicHue + (1 - OldHue))];
NScore = Math.min(NScore[0], NScore[1]);
OScore = Math.min(OScore[0], OScore[1]);
NewHue = rgbToHsv(imgData.data[i], imgData.data[i + 1], imgData.data[i + 2])[1];
PicHue = rgbToHsv(imgData2.data[i], imgData2.data[i + 1], imgData2.data[i + 2])[1];
OldHue = rgbToHsv(imgData3.data[i], imgData3.data[i + 1], imgData3.data[i + 2])[1];
NScore += Math.abs(NewHue-PicHue);
OScore += Math.abs(OldHue-PicHue);
NewHue = rgbToHsv(imgData.data[i], imgData.data[i + 1], imgData.data[i + 2])[2];
PicHue = rgbToHsv(imgData2.data[i], imgData2.data[i + 1], imgData2.data[i + 2])[2];
OldHue = rgbToHsv(imgData3.data[i], imgData3.data[i + 1], imgData3.data[i + 2])[2];
N += Math.abs(NewHue-PicHue) + NScore;
O += Math.abs(OldHue-PicHue) + OScore;
}
function rgbToHsv(r, g, b){
r = r/255, g = g/255, b = b/255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, v = max;
var d = max - min;
s = max == 0 ? 0 : d / max;
if(max == min){
h = 0; // achromatic
}else{
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, v];
}
function HSLCompare() {
var result = 0;
rgb = false;
var NewHue = rgbToHue(imgData.data[i], imgData.data[i + 1], imgData.data[i + 2])[0];
var PicHue = rgbToHue(imgData2.data[i], imgData2.data[i + 1], imgData2.data[i + 2])[0];
var OldHue = rgbToHue(imgData3.data[i], imgData3.data[i + 1], imgData3.data[i + 2])[0];
if (rgb == true) {
N += Math.abs(imgData.data[i] - imgData2.data[i]);
N += Math.abs(imgData.data[i + 1] - imgData2.data[i + 1]);
N += Math.abs(imgData.data[i + 2] - imgData2.data[i + 2]);
O += Math.abs(imgData3.data[i] - imgData2.data[i]);
O += Math.abs(imgData3.data[i + 1] - imgData2.data[i + 1]);
O += Math.abs(imgData3.data[i + 2] - imgData2.data[i + 2]);
return;
}
var NScore = [Math.abs(NewHue - PicHue), ((NewHue < PicHue) ? NewHue + (1 - PicHue) : PicHue + (1 - NewHue))];
var OScore = [Math.abs(OldHue - PicHue), ((OldHue < PicHue) ? OldHue + (1 - PicHue) : PicHue + (1 - OldHue))];
NScore = Math.min(NScore[0], NScore[1]);
OScore = Math.min(OScore[0], OScore[1]);
NewHue = rgbToHue(imgData.data[i], imgData.data[i + 1], imgData.data[i + 2])[1];
PicHue = rgbToHue(imgData2.data[i], imgData2.data[i + 1], imgData2.data[i + 2])[1];
OldHue = rgbToHue(imgData3.data[i], imgData3.data[i + 1], imgData3.data[i + 2])[1];
NScore += Math.abs(NewHue-PicHue);
OScore += Math.abs(OldHue-PicHue);
NewHue = rgbToHue(imgData.data[i], imgData.data[i + 1], imgData.data[i + 2])[2];
PicHue = rgbToHue(imgData2.data[i], imgData2.data[i + 1], imgData2.data[i + 2])[2];
OldHue = rgbToHue(imgData3.data[i], imgData3.data[i + 1], imgData3.data[i + 2])[2];
N += Math.abs(NewHue-PicHue) + NScore;
O += Math.abs(OldHue-PicHue) + OScore;
}
function rgbToHue(r, g, b) {
if (Math.max(r, g, b) - Math.min(r, g, b) < 50) {
rgb = true
}
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [h,s,l];
}
//Create a 4x4 ImageData object, random color selected from the colors var, transparent corners.
function paintBucket() {
color = Math.floor(Math.random() * 28);
imgData = draw.createImageData(4, 4);
imgData2 = draw.getImageData(x, y, 4, 4);
i = 0;
createCorn();
createColor();
createColor();
createCorn();
for (l = 0; l < 8; l++) {
createColor();
}
createCorn();
createColor();
createColor();
createCorn();
}
function createCorn() {
imgData.data[i] = imgData2.data[i];
imgData.data[i + 1] = imgData2.data[i + 1];
imgData.data[i + 2] = imgData2.data[i + 2];
imgData.data[i + 3] = 255;
i += 4;
}
function createColor() {
imgData.data[i] = colors[color][0];
imgData.data[i + 1] = colors[color][1];
imgData.data[i + 2] = colors[color][2];
imgData.data[i + 3] = 255;
i += 4;
}
<canvas id="canvas" hidden></canvas>
<br>
<canvas id="data" hidden></canvas>
<br>
<input type="file" id="file"></input>
<br>
<img id="img">
<img id="reload" hidden>
<p>Algorithms:</p>
<select id="selectCord">
<option value="1">Random</option>
<option value="2">Random Alligned</option>
<option value="3" selected>Loop</option>
</select>
<select id="selectColor">
<option value="2000">Super Speedy</option>
<option value="1000">Very Speedy</option>
<option value="500" selected>Speedy</option>
<option value="1">Visual</option>
</select>
<select id="colorDif">
<option value="RGB" selected>RGB</option>
<option value="HSL">HSL</option>
<option value="HSV">HSV</option>
</select>
<p>Algorithm Options:
<br>
</p>
<p>X Offset:
<input id="allignX" type="range" min="0" max="3" value="0"></input>
</p>
<p>Y Offset:
<input id="allignY" type="range" min="0" max="3" value="0"></input>
</p>
<p>Spacing X:
<input id="loopX" type="range" min="1" max="4" value="2"></input>
</p>
<p>Spacing Y:
<input id="loopY" type="range" min="1" max="4" value="2"></input>
</p>
RGB:
HSL:
HSV:
C# (reference implementation)
This is the code used to generate the images in the question. I thought it would be useful to give some people a reference for organizing their algorithm. A completely random coordinate and color are selected each movement. It performs surprisingly well considering the limitations imposed by the brush size/acceptance criteria.
I use the CIEDE2000 algorithm for measuring color differences, from the open source library ColorMine. This should give closer color matches (from a human perspective) but it doesn't seem to be a noticeable difference when used with this palette.
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using ColorMine.ColorSpaces;
using ColorMine.ColorSpaces.Comparisons;
namespace Painter
{
public class Painter
{
private readonly Bitmap _source;
private readonly Bitmap _painting;
private readonly int _width;
private readonly int _height;
private readonly CieDe2000Comparison _comparison = new CieDe2000Comparison();
private const int BRUSHSIZE = 4;
private readonly Random _random = new Random();
private readonly ColorPalette _palette;
private static readonly int[][] BRUSH = {
new[] {1, 0}, new[] {2, 0},
new[] {0, 1}, new[] {1, 1}, new[] {2, 1}, new[] {3, 1},
new[] {0, 2}, new[] {1, 2}, new[] {2, 2}, new[] {3, 2},
new[] {1, 3}, new[] {2, 3}
};
public Painter(string sourceFilename, string paletteFilename)
{
_source = (Bitmap)Image.FromFile(sourceFilename);
_width = _source.Width;
_height = _source.Height;
_palette = Image.FromFile(paletteFilename).Palette;
_painting = new Bitmap(_width, _height, PixelFormat.Format8bppIndexed) {Palette = _palette};
// search for black in the color palette
for (int i = 0; i < _painting.Palette.Entries.Length; i++)
{
Color color = _painting.Palette.Entries[i];
if (color.R != 0 || color.G != 0 || color.B != 0) continue;
SetBackground((byte)i);
}
}
public void Paint()
{
// pick a color from the palette
int brushIndex = _random.Next(0, _palette.Entries.Length);
Color brushColor = _palette.Entries[brushIndex];
// choose coordinate
int x = _random.Next(0, _width - BRUSHSIZE + 1);
int y = _random.Next(0, _height - BRUSHSIZE + 1);
// determine whether to accept/reject brush
if (GetBrushAcceptance(brushColor, x, y))
{
BitmapData data = _painting.LockBits(new Rectangle(0, y, _width, BRUSHSIZE), ImageLockMode.ReadWrite, PixelFormat.Format8bppIndexed);
byte[] bytes = new byte[data.Height * data.Stride];
Marshal.Copy(data.Scan0, bytes, 0, bytes.Length);
// apply 4x4 brush without corners
foreach (int[] offset in BRUSH)
{
bytes[offset[1] * data.Stride + offset[0] + x] = (byte)brushIndex;
}
Marshal.Copy(bytes, 0, data.Scan0, bytes.Length);
_painting.UnlockBits(data);
}
}
public void Save(string filename)
{
_painting.Save(filename, ImageFormat.Png);
}
private void SetBackground(byte index)
{
BitmapData data = _painting.LockBits(new Rectangle(0, 0, _width, _height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
byte[] bytes = new byte[data.Height * data.Stride];
for (int i = 0; i < data.Height; i++)
{
for (int j = 0; j < data.Stride; j++)
{
bytes[i*data.Stride + j] = index;
}
}
Marshal.Copy(bytes, 0, data.Scan0, bytes.Length);
_painting.UnlockBits(data);
}
private bool GetBrushAcceptance(Color brushColor, int x, int y)
{
double currentDifference = 0.0;
double brushDifference = 0.0;
foreach (int[] offset in BRUSH)
{
Color sourceColor = _source.GetPixel(x + offset[0], y + offset[1]);
Rgb sourceRgb = new Rgb {R = sourceColor.R, G = sourceColor.G, B = sourceColor.B};
Color currentColor = _painting.GetPixel(x + offset[0], y + offset[1]);
currentDifference += sourceRgb.Compare(new Rgb {R = currentColor.R, G = currentColor.G, B = currentColor.B}, _comparison);
brushDifference += sourceRgb.Compare(new Rgb {R = brushColor.R, G = brushColor.G, B = brushColor.B}, _comparison);
}
return brushDifference < currentDifference;
}
}
}
Then you can generate series of images (like my video) by calling an instance in a way similar to the code below (tweak based on number of iterations/frames/name desired). The first argument is the file path to the source image, the second argument is the file path to the palette (linked in the question), and the third argument is the file path for output images.
namespace Painter
{
class Program
{
private static void Main(string[] args)
{
int i = 0;
int counter = 1;
Painter painter = new Painter(args[0], args[1]);
while (true)
{
painter.Paint();
if (i%500000 == 0)
{
counter++;
painter.Save(string.Format("{0}{1:D7}.png", args[2], counter));
}
i++;
}
}
}
}
I searched for some colorful canvas paintings online and came across the images below, which seem to be great (complicated) test images. All copyrights belong to their respective owners.
Source
Source
Source