I did a quick sketch to illustrate the idea I had.
As you will see there are some artifacts in the renders and they are due to my implementation, but I don’t have the time to find the mistakes tonight.
The key idea is to use a quad tree to split the space into quads. Since you want smaller pixels in the center and bigger ones on the outside, you can generate a bunch of random points following the distribution that you want and add those point to your quad tree to split accordingly.
For the points distribution, I used this technique. Below is the code sample for the points distribution and an exemple result:
void setup() {
size(600, 600);
background(20);
for (long i = 0; i < 100000; i++) {
float r, a;
r = pickRandom() * width / sqrt(2);
a = random(TWO_PI);
PVector pt = new PVector(width / 2.0 + r * cos(a), height / 2.0 + r * sin(a));
stroke(230);
point(pt.x, pt.y);
}
}
float pickRandom() {
float r1, p, r2;
while(true) {
r1 = random(1);
p = 1 - pow(r1, 6);
r2 = random(1);
if (r2 < p) {
return r1;
}
}
}
Now, if we add those points into the quad tree and look at the partial distribution we have a result similar to this:
All is left to do is replace each quad by a rectangle colored with the image average color in that region. Which would give this:
Playing a bit with the parameters, it is also possible to get a less squary look by offsetting the creation of new quads within the tree. Something like that:
And the output:
As you can see, this is when the artifacts are appearing so there is room for improvement.
Anyway, if you want the full code, please find it below.
Code
PImage img;
TreeNode quadTree;
void setup() {
size(600, 600); //Input the same dimension as the final image
background(20);
img = loadImage("cat600.jpg"); // Chose the image that you want to use
quadTree = new TreeNode(new Coord(0, 0), new Coord(img.width - 1, img.height - 1));
for (long i = 0; i < 100000; i++) {
float r, a;
r = pickRandom() * width / sqrt(2);
a = random(TWO_PI);
PVector pt = new PVector(width / 2.0 + r * cos(a), height / 2.0 + r * sin(a));
quadTree.add(pt);
}
img.loadPixels();
//quadTree.display();
quadTree.displayFromPic();
}
float pickRandom() {
float r1, p, r2;
while(true) {
r1 = random(1);
p = 1 - pow(r1, 6);
r2 = random(1);
if (r2 < p) {
return r1;
}
}
}
class Coord{
public int x, y;
Coord(int x, int y) {
this.x = x;
this.y = y;
}
}
class AABB
{
private Coord TL;
private Coord BR;
private Coord midPt;
private boolean isDivisible;
AABB(Coord TL, Coord BR) {
this.TL = TL;
this.BR = BR;
midPt = new Coord((int)((BR.x + TL.x) / 2.0), (int)((BR.y + TL.y) / 2.0));
isDivisible = true;
if ((int)(BR.x - TL.x) == 1 || (int)(BR.y - TL.y) == 1)
isDivisible = false;
}
public boolean contains(PVector pt) {
if (pt.x < TL.x || pt.x >= BR.x)
return false;
if (pt.y < TL.y || pt.y >= BR.y)
return false;
return true;
}
public boolean isDivisible() {
return isDivisible;
}
public Coord getMidPt() {
return midPt;
}
public Coord TL() {
return TL;
}
public Coord BR() {
return BR;
}
public void display() {
noFill();
stroke(230);
strokeWeight(1);
line(TL.x, TL.y, BR.x, TL.y);
line(TL.x, BR.y, TL.x, TL.y);
}
public void displayFromPic() {
float r = 0;
float g = 0;
float b = 0;
for (int i = TL.x; i <= BR.x; i++) {
for (int j = TL.y; j <= BR.y; j++) {
int idx = j * img.width + i;
color col = img.pixels[idx];
r += ((col >> 16) & 0xFF);
g += ((col >> 8) & 0xFF);
b += (col & 0xFF);
}
}
float divider = (BR.x - TL.x + 1) * (BR.y - TL.y + 1);
r /= divider;
g /= divider;
b /= divider;
for (int i = TL.x; i <= BR.x; i++) {
for (int j = TL.y; j <= BR.y; j++) {
int idx = j * img.width + i;
pixels[idx] = color(r, g, b);
}
}
}
}
class TreeNode
{
private final int NODE_CAPACITY = 15;
private final float minRatio = 0.35;
private final float maxRatio = 0.65;
private AABB boundary;
private ArrayList<PVector> pts;
private TreeNode TLnode;
private TreeNode TRnode;
private TreeNode BLnode;
private TreeNode BRnode;
TreeNode (Coord TL, Coord BR) {
boundary = new AABB(TL, BR);
initNode();
}
TreeNode (int x1, int y1, int x2, int y2) {
boundary = new AABB(new Coord(x1, y1), new Coord(x2, y2));
initNode();
}
private void initNode() {
pts = new ArrayList<PVector>();
}
public void add(PVector pt) {
if (!boundary.isDivisible())
return;
if (!boundary.contains(pt))
return;
if (pts.size() < NODE_CAPACITY && TLnode == null) {
pts.add(pt);
return;
}
if (TLnode == null)
subdivide();
addToChildren(pt);
}
private void addToChildren(PVector pt) {
if (TLnode.contains(pt))
TLnode.add(pt);
if (TRnode.contains(pt))
TRnode.add(pt);
if (BLnode.contains(pt))
BLnode.add(pt);
if (BRnode.contains(pt))
BRnode.add(pt);
}
private void subdivideClassic() {
Coord TL = boundary.TL();
Coord midPt = boundary.getMidPt();
Coord BR = boundary.BR();
TLnode = new TreeNode(TL, midPt);
TRnode = new TreeNode(midPt.x + 1, TL.y, BR.x, midPt.y);
BLnode = new TreeNode(TL.x, midPt.y + 1, midPt.x, BR.y);
BRnode = new TreeNode(midPt.x + 1, midPt.y + 1, BR.x, BR.y);
for (int i = pts.size() - 1; i > -1; i--) {
addToChildren(pts.get(i));
pts.remove(i);
}
}
private void subdivide() {
Coord TL = boundary.TL();
Coord BR = boundary.BR();
if (random(1) < 0.5) {
int x = (int)(TL.x + random(minRatio, maxRatio) * (BR.x - TL.x));
int y1 = (int)(TL.y + random(minRatio, maxRatio) * (BR.y - TL.y));
int y2 = (int)(TL.y + random(minRatio, maxRatio) * (BR.y - TL.y));
TLnode = new TreeNode(TL.x, TL.y, x, y1);
TRnode = new TreeNode(x + 1, TL.y, BR.x, y2);
BLnode = new TreeNode(TL.x, y1 + 1, x, BR.y);
BRnode = new TreeNode(x + 1, y2 + 1, BR.x, BR.y);
} else {
int x1 = (int)(TL.x + random(minRatio, maxRatio) * (BR.x - TL.x));
int x2 = (int)(TL.x + random(minRatio, maxRatio) * (BR.x - TL.x));
int y = (int)(TL.y + random(minRatio, maxRatio) * (BR.y - TL.y));
TLnode = new TreeNode(TL.x, TL.y, x1, y);
TRnode = new TreeNode(x1 + 1, TL.y, BR.x, y);
BLnode = new TreeNode(TL.x, y + 1, x2, BR.y);
BRnode = new TreeNode(x2 + 1, y + 1, BR.x, BR.y);
}
for (int i = pts.size() - 1; i > -1; i--) {
addToChildren(pts.get(i));
pts.remove(i);
}
}
public boolean contains(PVector pt) {
return boundary.contains(pt);
}
public void display() {
if (TLnode == null) {
boundary.display();
return;
}
TLnode.display();
TRnode.display();
BLnode.display();
BRnode.display();
}
public void displayFromPic() {
if (TLnode == null) {
boundary.displayFromPic();
return;
}
TLnode.displayFromPic();
TRnode.displayFromPic();
BLnode.displayFromPic();
BRnode.displayFromPic();
}
}
Edit:
An example a bit more extreme:
Edit2: Corrected the rendering issue: