C++ - somewhat random lines and then some
First some random lines
The first step of the algorithm randomly generates lines, takes for the target image an average of the pixels along this, and then calculates if the summed up square of rgb space distances of all pixels would be lower if we would paint the new line (and only paint it, if it is). The new lines color for this is chosen as the channel wise average of the rgb values, with a -15/+15 random addition.
Things that I noticed and influenced the implementation:
- The initial color is the average of the complete image. This is to counter funny effects like when making it white, and the area is black, then already something as a bright green line is seen better, as it is nearer to black than the already white one.
- Taking the pure average color for the line is not so good as it turns out to be unable to generate highlights by being overwritten by later lines. Doing a little random deviation helps a bit, but if you look at starry night, it fails if the local contrast is high at many places.
I was experimenting with some numbers, and chose L=0.3*pixel_count(I)
and left m=10
and M=50
. It will produce nice results starting at around 0.25
to 0.26
for the number of lines, but I chose 0.3 to have more room for accurate details.
For the full sized golden gate image, this resulted in 235929 lines to paint (for which it took a whopping 13 seconds here). Note that all images here are displayed in reduced size and you need to open them in a new tab/download them to view the full resolution.
Erase the unworthy
The next step is rather expensive (for the 235k lines it took about an hour, but that should be well within the "an hour for 10k lines on 1 megapixel" time requirement), but it is also a bit surprising. I go through all the previously painted lines, and remove those that do not make the image better. This leaves me in this run with only 97347 lines that produce the following image:
You probably need to download and compare them in an appropriate image viewer to spot most differences.
and start over again
Now I have a lot of lines that I can paint again to have a total of 235929 again. Not much to say, so here is the image:
short analysis
The whole procedure seems to work like a blurring filter that is sensitive to local contrast and object sizes. But it is also interesting to see where the lines are painted, so the program records these too (For each line, the pixel color will be made one step whiter, at the end the contrast is maximized). Here are the corresponding ones to the three colored above.
animations
And since we all love animations, here are some animated gifs of the whole process for the smaller golden gate image. Note that there is significant dithering due to gif format (and since creators of true color animation file formats and browser manufacturers are in war over their egos, there is no standard format for true color animations, otherwise I could have added an .mng or similar).
Some more
As requested, here are some results of the other images (again you might need to open them in a new tab to not have them downscaled)
Future thoughts
Playing around with the code can give some intresting variations.
- Chose the color of the lines by random instead of being based on the average. You might need more than two cycles.
- The code in the pastebin also contains some idea of a genetic algorithm, but the image is probably already so good that it would take too many generations, and this code is also too slow to fit into the "one hour" rule.
- Do another round of erase/repaint, or even two...
- Change the limit of where lines can be erased (e.g. "must make the image at lest N better")
The code
These are just the two main useful functions, the whole code doesn't fit in here and can be found at http://ideone.com/Z2P6Ls
The bmp
classes raw
and raw_line
function do access pixels and lines respectively in an object that can be written to bmp format (It was just some hack lying around and I thought that makes this somewhat independent from any library).
The input file format is PPM
std::pair<bmp,std::vector<line>> paint_useful( const bmp& orig, bmp& clone, std::vector<line>& retlines, bmp& layer, const std::string& outprefix, size_t x, size_t y )
{
const size_t pixels = (x*y);
const size_t lines = 0.3*pixels;
// const size_t lines = 10000;
// const size_t start_accurate_color = lines/4;
std::random_device rnd;
std::uniform_int_distribution<size_t> distx(0,x-1);
std::uniform_int_distribution<size_t> disty(0,y-1);
std::uniform_int_distribution<size_t> col(-15,15);
std::uniform_int_distribution<size_t> acol(0,255);
const ssize_t m = 1*1;
const ssize_t M = 50*50;
retlines.reserve( lines );
for (size_t i = retlines.size(); i < lines; ++i)
{
size_t x0;
size_t x1;
size_t y0;
size_t y1;
size_t dist = 0;
do
{
x0 = distx(rnd);
x1 = distx(rnd);
y0 = disty(rnd);
y1 = disty(rnd);
dist = distance(x0,x1,y0,y1);
}
while( dist > M || dist < m );
std::vector<std::pair<int32_t,int32_t>> points = clone.raw_line_pixels(x0,y0,x1,y1);
ssize_t r = 0;
ssize_t g = 0;
ssize_t b = 0;
for (size_t i = 0; i < points.size(); ++i)
{
r += orig.raw(points[i].first,points[i].second).r;
g += orig.raw(points[i].first,points[i].second).g;
b += orig.raw(points[i].first,points[i].second).b;
}
r += col(rnd);
g += col(rnd);
b += col(rnd);
r /= points.size();
g /= points.size();
b /= points.size();
r %= 255;
g %= 255;
b %= 255;
r = std::max(ssize_t(0),r);
g = std::max(ssize_t(0),g);
b = std::max(ssize_t(0),b);
// r = acol(rnd);
// g = acol(rnd);
// b = acol(rnd);
// if( i > start_accurate_color )
{
ssize_t dp = 0; // accumulated distance of new color to original
ssize_t dn = 0; // accumulated distance of current reproduced to original
for (size_t i = 0; i < points.size(); ++i)
{
dp += rgb_distance(
orig.raw(points[i].first,points[i].second).r,r,
orig.raw(points[i].first,points[i].second).g,g,
orig.raw(points[i].first,points[i].second).b,b
);
dn += rgb_distance(
clone.raw(points[i].first,points[i].second).r,orig.raw(points[i].first,points[i].second).r,
clone.raw(points[i].first,points[i].second).g,orig.raw(points[i].first,points[i].second).g,
clone.raw(points[i].first,points[i].second).b,orig.raw(points[i].first,points[i].second).b
);
}
if( dp > dn ) // the distance to original is bigger, use the new one
{
--i;
continue;
}
// also abandon if already too bad
// if( dp > 100000 )
// {
// --i;
// continue;
// }
}
layer.raw_line_add(x0,y0,x1,y1,{1u,1u,1u});
clone.raw_line(x0,y0,x1,y1,{(uint32_t)r,(uint32_t)g,(uint32_t)b});
retlines.push_back({ (int)x0,(int)y0,(int)x1,(int)y1,(int)r,(int)g,(int)b});
static time_t last = 0;
time_t now = time(0);
if( i % (lines/100) == 0 )
{
std::ostringstream fn;
fn << outprefix + "perc_" << std::setw(3) << std::setfill('0') << (i/(lines/100)) << ".bmp";
clone.write(fn.str());
bmp lc(layer);
lc.max_contrast_all();
lc.write(outprefix + "layer_" + fn.str());
}
if( (now-last) > 10 )
{
last = now;
static int st = 0;
std::ostringstream fn;
fn << outprefix + "inter_" << std::setw(8) << std::setfill('0') << i << ".bmp";
clone.write(fn.str());
++st;
}
}
clone.write(outprefix + "clone.bmp");
return { clone, retlines };
}
void erase_bad( std::vector<line>& lines, const bmp& orig )
{
ssize_t current_score = evaluate(lines,orig);
std::vector<line> newlines(lines);
uint32_t deactivated = 0;
std::cout << "current_score = " << current_score << "\n";
for (size_t i = 0; i < newlines.size(); ++i)
{
newlines[i].active = false;
ssize_t score = evaluate(newlines,orig);
if( score > current_score )
{
newlines[i].active = true;
}
else
{
current_score = score;
++deactivated;
}
if( i % 1000 == 0 )
{
std::ostringstream fn;
fn << "erase_" << std::setw(6) << std::setfill('0') << i << ".bmp";
bmp tmp(orig);
paint(newlines,tmp);
tmp.write(fn.str());
paint_layers(newlines,tmp);
tmp.max_contrast_all();
tmp.write("layers_" + fn.str());
std::cout << "\r i = " << i << std::flush;
}
}
std::cout << "\n";
std::cout << "current_score = " << current_score << "\n";
std::cout << "deactivated = " << deactivated << "\n";
bmp tmp(orig);
paint(newlines,tmp);
tmp.write("newlines.bmp");
lines.clear();
for (size_t i = 0; i < newlines.size(); ++i)
{
if( newlines[i].is_active() )
{
lines.push_back(newlines[i]);
}
}
}
To clarify: Are libraries like SimpleCV allowed? And answers may have any choice for I, L, m, and M, including m=0 and L=area?
– rationalis – 2014-08-14T07:03:40.317@epicwisdom Yes, all libraries (except things that already specifically do this task) are allowed. Feel free to use keypoints, edge detection, whatever. Your algorithm should work for any valid choices of I, L, m, M, include m = 0 and L = area. (Though of course, your algorithm may look better for particular tunings of the parameters.) – Calvin's Hobbies – 2014-08-14T07:11:10.690
Then, for instance, this particular library algorithm would be considered an invalid answer?
– rationalis – 2014-08-14T07:34:16.547@epicwisdom Actually I will allow that and other similar things. It looks like it'd still take some clever tweaking to make an image out of the lines it gives you. – Calvin's Hobbies – 2014-08-14T07:40:36.637
1Do lines need to have thickness 1? – aditsu quit because SE is EVIL – 2014-08-17T20:42:47.617
@aditsu Yes, I should have made that clear, thanks. – Calvin's Hobbies – 2014-08-17T21:24:18.330
Hm, what to do with submissions where the code is too big? It is probably not the most brilliant piece, but the code block feature even inflates the already 26k to over 43k... – PlasmaHH – 2014-09-29T14:50:18.813
@PlasmaHH Post the most pertinent parts and say you didn't have room for all in your answer. Perhaps include the entire code in a pastebin or two. – Calvin's Hobbies – 2014-09-29T22:08:21.533