Different pixel sizes in one image - radial small to big

Hi!
I would like to rasterize an image into pixels, but have struggles to find the right code for what I am looking for: I would like it to have a very high resolution in the middle and then kind of “fall apart” into bigger pixels towards the outside.

Is there anyone who has experience with that? I would be extremely thankful!

1 Like

Hi @AntoniaVincenza, and welcome to the Processing Community!

Are you considering sampling the colors of pixels in the image, and then drawing a circle of the same color as that pixel, and at that same location, with the size of each circle based on the distance of the sampled pixel from the center of the image? You could sample the pixels randomly, or choose them in a more ordered manner.

Let’s also see what alternative ideas some other users might offer.

EDIT (October 24, 2021):

Please see this example:

You can begin with that example, then modify it so that the sizes of the ellipses are based on their distances from the center of the image.

2 Likes

Hi @AntoniaVincenza,

Welcome to the forum! :wink:

Challenge accepted!

Like Javagar said pointillism is a good way to approach the problem. The idea is that you sample points on your target image and use the pixel of that color.

You want a high resolution in the middle which fades away as soon as you go out from the center. You need to sample more points in the middle of the image than on the outside.

You need a sample distribution that has a circular falloff.

One way of doing this is to divide a circle into slices and randomly put squares on those steps with increasing size:

You then sample those points on the target image (picture taken from here):

If you put enough divisions and increase the number of squares you get this result which is pretty cool:

(note that the bigger pixels are drawn first so they don’t cover the smaller ones)

This method is not perfect since there are some gaps and the pixels don’t perfectly touch but this can be a style.

Another example with more samples and opacity:

Have fun! :grinning:

4 Likes

Thanks for the outstanding examples, @josephh! They are the quintessential “cat’s meow”!

2 Likes

Wow thank you for the quick reply!
Sounds logical, I will give it a try!
Do you have any suggestions where I can find examples of existing code? Since I’m very new to the program I‘m always a bit scared of writing my own from scratch.
Thanks again!

1 Like

Amazing, thank you so much for the detailed description!! This is exactly what I am looking for :slight_smile:
In my head the pixels are still laying in a grid, this I would probably set in the beginning right? Thanks again!!

2 Likes

You are welcome!

I thought about this but you need to find a way to lay down square pixels so that they don’t overlap and the density is increasing close to the center… Maybe another challenge :wink:

2 Likes

I’m giving it a go tonight. I have a nice idea I think. I will let you know how it goes =))

3 Likes

There are lots of great tutorials here, including videos and examples of code, for learning Processing from the start:

2 Likes

Wow amazing, thanks a lot!!:slight_smile:

1 Like

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:


4 Likes

This one was created with Processing Python Mode.
python_onion_pixellation
The original image is from here:

Credit for original image: © User:Colin / Wikimedia Commons

To pixellate the image, a focus point was chosen near the center of each onion. Looping though each pixel in the image, the coordinates and color were saved, along with the size, which was mapped according to the distance from the closest focus. The smaller the distance, the smaller the pixel.

Here’s the code:

import random
"""
 * Pointillism
 * by Daniel Shiffman.
 * Modified by javagar - October 25, 2021
 * 
 * Creates a simple pointillist effect using ellipses colored
 * according to pixels in an image. 
"""
smallPoint = 2
largePoint = 330
pixel_list = []

# foci, where the smallest pixels are drawn,
# are near the centers of the onions
foci = [(138, 130),
        (222, 93),
        (224, 162),
        (237, 232),
        (139, 224),
        (79, 189)]

def setup():
    size(330, 330)
    global img
    img = loadImage("330px-Mixed_onions.jpeg")
    rectMode(CENTER)
    noStroke()
    background(0)
    noLoop()

def draw():
    # determine location and color of pixel
    for x in range(width):
        for y in range(height):
            pix = img.get(x, y)
    
            # determine smallest dist from (x, y) to a focus, among all foci
            min_dist = width + height
            for f in foci:
                d = dist(f[0], f[1], x, y)
                if d < min_dist:
                    min_dist = d
            
            # min_dist determines point size
            pointillize = map(min_dist ** 2, 0, width * height, smallPoint, largePoint)
            fill_color = pix
    
            pixel_list.append((x, y, pointillize, fill_color))
    random.shuffle(pixel_list)
    for x, y, pointillize, fill_color in pixel_list:
        fill(fill_color)
        rect(x, y, pointillize, pointillize)
3 Likes

@javagar @jb4x Thank you this is so nice! :heart_eyes:

Same idea here with a quad tree but I am recursively dividing squares depending on the distance from the center:

quad_circle

This is the result when mapped onto a real image:

quad_circle

With a bit of randomness, you can get the following results:

The pseudo code is:

void recursiveSubdivide(float depth, Square square) {
  float distance = Compute distance from the ellipse to the square
  float divisions = map the distance to a threshold value
  
  // Do we stop the recursion?
  if (depth >= divisions + randomness) {
    Add the square to the list of squares
    return;
  }
  
  // Recursively call the function for each sub square
  recursiveSubdivide(depth + 1, new Square(square.x, square.y, half));
  recursiveSubdivide(depth + 1, new Square(square.x + half, square.y, half));
  recursiveSubdivide(depth + 1, new Square(square.x, square.y + half, half));
  recursiveSubdivide(depth + 1, new Square(square.x + half, square.y + half, half));
}
4 Likes

This is amazing, exactly what I was imagining! Huge thanks and shoutout to you!!

2 Likes

So great! Love the onions lol :grin:

1 Like

Wow awesome! Thanks for spending all the time on it and the detailed description, it’s really close to what I am looking for :))

3 Likes

Next step is to paint a grey scale subdivision map that can be edited live and user gets direct feedback of the result :heart_eyes:

Love your take on it by the way!

2 Likes

For anyone who wishes to start with a very basic radial pixellation, here’s a simple sketch, created with Processing Python Mode:
python_pixellated_jupiter
The original image is from here:

Here’s the code:

# Radial pixellation of Jupiter
# @ javagar - October 26, 2021
def setup():
    size(330, 330)
    global img
    # image from https://en.wikipedia.org/wiki/Jupiter
    img = loadImage("Jupiter_and_its_shrunken_Great_Red_Spot.jpeg")
    rectMode(CENTER)
    noStroke() # do not draw stroke; only draw fill
    noLoop() # draw() will run only once
    
def draw():
    global img
    # coordinates of the center of the image and the canvas
    center_x, center_y = width / 2, height / 2
    # display the image
    image(img, 0, 0)
    # process random pixels
    for i in range(10000):
        # identify a random pixel
        x, y = int(random(img.width)), int(random(img.height))
        # get color of the pixel
        pix = get(x, y)
        # set fill to color of the pixel
        fill(pix)
        # get distance of pixel from center
        d = dist(center_x, center_y, x, y)
        # draw square; size controlled by distance from center
        square(x, y, d / 20)

See the embedded comments for explanations of the code.

Feel free to copy and modify this for your own education and use, and of course, feel free to ask questions.

1 Like