poisson-disc-image

Poisson Disc Sampling with python

12:51 PM, November-03-2022

#computer graphic #Math #Python #C#

Joseph Bakulikira


Hello Everyone, Welcome to this article about Poisson Disk sampling.

Youtube video: https://youtu.be/kzPmjAhBNfY

For a Deeper understanding and explanation , I strongly recommend to read this paper or atleast take a look at it:

https://www.cs.ubc.ca/~rbridson/docs/bridson-siggraph07-poissondisk.pdf

Poisson-disk sampling is a random process for selecting points from a subdomain of a metric space. A selected point must be disk-free,
at least a minimum distance, r, from any previously selected point. Thus each point has an associated disk of radius r that precludes the selection of nearby points. The selected points are called a sample or distribution.

In this article i'm gonna just try to explain the python implementation , but i'm gonna leave the link for both the python and C# code at the end of this article.

for this python implementation we gonna use pygame:

if you don't have pygame , you can install it just by running this command in your terminal

'pip install pygame'.

So first we gonna make a new file 'sample.py' which gonna have Vector class that's gonna store the direction of the vector but also it gonna have two functions , one to normalize the vector and another to get the magnitude of the vector.

import math
class Vector2:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def normalize_vector(self):
        magnitude = math.sqrt(self.x * self.x + self.y * self.y)
        self.x = self.x/magnitude
        self.y = self.y/magnitude


    def set_magnitude(self, new_magnitude):
        self.normalize_vector()
        x = self.x * new_magnitude
        y = self.y * new_magnitude

        return Vector2(x, y)

Now we can make our root file 'main.py'.

First let import the libraries that we gonna use and initialize pygame and some constant variables that we gonna use such as the width and the height of the screen

import pygame
import math
import random
from Sample import Vector2
import colorsys

width, height = 1920, 1080
size=(width, height)
black, white, green = (10, 10, 10), (230, 230, 230), (95, 255, 1)
hue = 0

pygame.init()
screen = pygame.display.set_mode(size)
clock = pygame.time.Clock()
fps = 60

let's initialize the variables that we gonna use like the grid lists which gonna store all our samples. and also iniatialize a vector of a random direction.

x = random.randint(50, width-50)
y = random.randint(50, height-50)
position = Vector2(x, y)

cl = x // w
rw = y // w
columns = width // w
rows = height // w
active_list = []

grid = [i for i in range(math.ceil(columns * rows))]

for i in range(math.ceil(columns * rows)):
    grid[i] = None
grid[math.ceil(cl+rw*columns)] = position
active_list.append(position)

we gonna also make a function that convert hsv colors to rgb colors since we need a smooth changing color of our disc or points and a splice list function.

def list_splice(target, start, delete_count=None, *items):
    if delete_count == None:
        delete_count = len(target) - start

    total = start + delete_count
    removed = target[start:total]
    target[start:total] = items
    return removed

def hsv_to_rgb(h, s, v):
    return tuple(round(i * 255) for i in colorsys.hsv_to_rgb(h, s, v))

Now we can make the main loop of our program,

run = True
while run:
    # set framerate
    clock.tick(fps)
    # clear screen color
    screen.fill(black)
    
    # handle user input
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                run = False
    
    
    if len(active_list) > 0:
        randIndex = random.randint(0, len(active_list)-1)
        current_position = active_list[randIndex]
        found = False
        for n in range(k):
            offset = Vector2(random.uniform(-2, 2), random.uniform(-2, 2))
            new_magnitude = random.randint(r, r*2)
            offset = offset.set_magnitude(new_magnitude)
            offset.x = offset.x + current_position.x
            offset.y = offset.y + current_position.y

            col = math.ceil(offset.x/w)
            row = math.ceil(offset.y/w)

            if row < rows-screen_offset and col < columns-screen_offset and row > screen_offset and col > screen_offset:
                checker = True
                for i in range(-1, 2):
                    for j in range(-1, 2):
                        index = math.ceil( col + i + (row+j) * columns)

                        neighbour = grid[index];
                        if neighbour is not None:
                            dist = math.sqrt((offset.x - neighbour.x) ** 2 + (offset.y - neighbour.y) ** 2)
                            if dist < r:
                                checker = False


                if checker is True:
                    found = True
                    grid[math.ceil(col + row * columns)] = Vector2(offset.x, offset.y)
                    active_list.append(Vector2(offset.x, offset.y))
                    break
        if found is not True:
            list_splice(active_list, randIndex+1, 1)
    
    # draw all the sample in the grid
    for cell in grid:
        if cell is not None:
            pygame.draw.circle(screen, white, (math.ceil(cell.x), math.ceil(cell.y)), 16)

    for disk in active_list:
        pygame.draw.circle(screen, hsv_to_rgb(hue, 1, 1), (math.ceil(disk.x), math.ceil(disk.y)), 16)

    pygame.display.flip()
    hue += 0.0009
pygame.quit()

you can now run you main file to see the result

poisson-image
Github code python version: Poisson disc sampling with python
Github code C# version: Poisson disc sampling with c#
Youtube channel: Auctux

thank you ✌️