Glyph Generator

For those that were following my Glyph Generator project, it’s now live at GitHub: Glyph-Generator

The Glyph Generator project began as an effort to create visually compelling glyphs for the Glow Cuboid, leveraging AI to assist with random generation, classification, and artistic processing. However, it has now evolved into a standalone system for glyph generation, including random glyph creation, extracting glyphs from Unicode fonts, machine learning-based classification, and applying artistic transformations to create captivating outputs.

This system is modular and can be used for artistic, functional, or experimental purposes, such as creating custom art or visualizations. Below is a small example of computer generated glyphs.

4 Likes

Hi @MarkMakies

That’s a really cool project! Thanks so much for posting it, hopefully there is someone out there who can make use of it.

2 Likes

It looks interesting. Thanks.

My next step is to get these glyphs onto a LED panel. The Glow Cuboid was only designed for 8x8, so I’ve printed a new diffuser that will handle 4 matrix modules for a 16x16 display - first prototype in pic just came off the printer.

Each glyph will be represented by only 256 bits, so I can store lots of glyphs with little memory, about 32,000 per MB on the MCU, so a plain old RP2040 should do the trick. It will be pretty much the same code as the Glow Cuboid, especially when it comes to the random colour generation algorithm.

The symbols however won’t be randomly generated, but randomly selected, from those preloaded, and then rendered on the the display. This should give me the ‘alien’ like script I was hoping for when I first created the Cuboid in 2023.

2 Likes

early simulation of possible rendering

4 Likes

Hi Mark.

This is really cool.
I like your half AI, half real time classification approach.


You might be interested in this project from a few years ago where I workshop a similar problem.
Similar vision.

Here is an example of an algorithmically generated bitmap.

Looking awesome.
I always love seeing your projects evolve.
Thanks for sharing.
Pix :heavy_heart_exclamation:

1 Like

Hey Pix, glad you like the project. I had at look at gen_me_a_pixamon .

Ahh, Rust, never heard of it until now, steep learning curve by the looks of it. I was hoping to give it a try, but my programming skills are stuck in Python.

Otherwise this would be an ideal project for playing with Machine Learning algorithms.

mark

1 Like

If, after you’ve got some glyphs going on glowbits, should you still be curious about the idea, I’d be happy to port it to python for you.

The only reason I wrote it in rust was so I could compile it to a binary and punch it to my server. Rust isn’t doing anything magic here and python is plenty fast enough for this task.
:crab: :arrow_forward: :snake:

2 Likes

Hey Pix,
I’ve got my 16x16 panel going well. If you’ve got the time, please port to python and i’ll give it a crack.

2 Likes

I’m curious to see it so as long as you take photos I’m happy to do it.
I’m guessing you need the monster to be represented as a numpy array?

monster = np.empty((16, 16, 3), dtype=np.uint8) #16*16 GRB pixels?

Alternatively should I output a custom object you’ve made? A preferred data-structure?

1 Like

My coding is pretty basic, I just use the neopixel object, which is a list of tuples I guess. I’m pretty sure I’ll be able top work out a conversion from whatever you have.

NUM_PIXELS = 256
Neo = neopixel.NeoPixel(NEO_PIN, NUM_PIXELS)
Neo[i] = (R,G,B) # where i, R, G, B are all between 0 and 255

I’ll be able to get some pics / videos for you. Both from the real matrix and diffuser, and pics from my simulator.

ATM I’m just battling 3D printer calibration for eSun PLA+HS (which can be printed at 350mm/s !!) as they’re phasing out PLA+ which is getting harder to get. The properties are very different and oozing and stringing have been problematic, especially on my opaque 16x16 diffuser. I’ll get it sorted soon.

1 Like

16x16 diffuser and stand done, all ready for “monsters”

Below pic taken with an iPhone8 so quality is not so good. For the final version I’ll use an SLR like I did for the Glow Cuboid videos, it will provide a much better colour rendition and definition, unlike the Glow Cuboid due to the frosted perspex filter.

4 Likes

Working on it.
I’m fighting the interpreter which is very keen to convert all the pixel data into floats and linked lists.
I’ll figure it out eventually. :slight_smile:
Gimme another weekend at it.

Looks awesome :+1:

3 Likes

Hey folks, if anyone wants to make one of these or just generate glyphs for some art, detailed instructions are now up on Instructables:

3 Likes

Update on pixamons.

I’ve finally pushed python to it’s breaking point. :partying_face:
Some notable changes

  1. I’ve hard coded slightly different weights to make it a bit more expressive at 16x16 pixels.
  2. I’ve also ditched all the code to do with proximal interpolation
  3. I’ve added the simple function print_monster() in case your in an environment without a X11 and you need to debug.

I’m happy for this python port to fall under the same MIT license that the original rust code.

Code below.

from random import random, seed, choice, uniform
from math import floor, ceil
import numpy as np
from PIL import Image

class Vec2:
	#A 2d Vector
	def __init__(self, x, y):
		self.x = x
		self.y = y

def print_monster(monster):
	#Cmd line render of the monster
	for i in range(monster.shape[0]):
		for j in range(monster.shape[1]):
			if np.any(monster[i, j] > 0):
				print("*", end="")
			else:
				print(" ", end="")
		print()

def fmap(a_rge, b_rge, c):
	#maps C from range A0-A1 to range B0-B1
    return b_rge.x + (c - a_rge.x) * (b_rge.y - b_rge.x) / (a_rge.y - a_rge.x)

def make_monster(mon_seed, size, loc):
	'''
	Construct an pixmon 
		args ->
			mon_seed : the seed for the random number generator
			size : width & height of the numpy array
			loc : Vector of top center point of the pixmon
		returns -> 
			A 3d numpy array of u8s with shape [width][height][rgb]
	'''

	#initialise the rng
	seed(mon_seed)

	#Blank Datastructure for our monster
	monster = np.zeros((size, size, 3), dtype=np.uint8)

	#Chance to flip axis for a bit more variety
	flip_axis = choice([True, False])

	#width and height of draw space
	w = size - 3 if flip_axis else size/2 - 1
	h = size - 3 if not flip_axis else size/2 - 1

	#Generate sensible biases... just the right amount of chaos
	normal = Vec2(0.0, 1.0)
	density = fmap(normal, Vec2(0.8, 0.9), random())
	y_bias = fmap(normal, Vec2(0.2, -0.2), random())
	colour_rand = fmap(normal, Vec2(0.75, 0.95), random())

	for k in range(0, int(w*h)):
		#We need our globals, depending on if we flipped
		i = k/w if flip_axis else k % w
		j = k/w if not flip_axis else k % w

		#Let's put our biases to work
		is_hole = random() > density
		a_scalar = floor(uniform(0, size/2)) * floor(uniform(0, size/2))
		y_scalar = floor(1.0 - 2.0 * y_bias)
		xy_scalar = pow(i, 2) + pow(j - y_scalar * (h/2), 2)
		bias = a_scalar > xy_scalar

		#If we're in the sweet spot
		if bias and not is_hole:

			# Indices
			pxl = floor(loc.x - int(i)) % size #Prime X axis
			pxr = floor(loc.x + int(i)) % size #Mirror X axis
			py = floor(loc.y + int(j)) % size #Row

			#Get the colour based on colour_rand (which is based on mon_seed)
			colour = np.random.randint(0, int(255 * colour_rand), size=(3,)) 

			#Paint it symetrically
			monster[pxl][py] = colour
			monster[pxr][py] = colour

	#Looks better 90deg right
	monster = np.rot90(monster, k=-1, axes=(0, 1))

	return monster

if __name__ == '__main__':
	#Monster Args
	size = 16
	seed_val = 80
	location = Vec2(int(size/2), 3)

	#Make a Monster
	data = make_monster(seed_val, size, location)
	printMonster(data)

If required you can render then numpy as an image on your screen with this code.

#create an image of that monster
img = Image.fromarray(make_monster(s, d, l), 'RGB')
img.show()s

I’d love to see how it looks @MarkMakies
Pix :heavy_heart_exclamation:

2 Likes

@Pixmusix I’ll get on to it soon.

2 Likes

Hey @Pixmusix, awesome port - it worked first shot! I’ve got it integrated with my glyph matrix simulator, and it’s running smoothly.

From here, I see two possible directions we can take:

  1. Pre-generate monster glyphs on PC: We could create a batch of around 10k monster glyphs by iterating through seeds and save them in a compressed binary file. This would then be uploaded to the hardware, where the MCU would handle random displays just like my existing glyphs. The glow matrix MCU code currently only uses binary for on/off states and generates colours onboard, so I’d need to tweak it to ensure symmetry, keeping in line with your original vision.

  2. Run the entire code directly on the MCU: However, I don’t think NumPy is compatible with the RP2040, so we’d need to rewrite the generation logic without it.

Let me know your thoughts!

# This software is a derivative work of original code by Jonny J Watson, licensed under the MIT License.
# The derivative portions of this software, created by Mark Makies, licensed under CC BY-SA 4.0.
#
# Copyright (c) 2023 Jonny J Watson
# MIT License (https://opensource.org/licenses/MIT)
# Original source: https://github.com/pixmusix/gen_me_a_pixamon
#
# Copyright (c) 2024 Mark Makies
# CC BY-SA 4.0 License (http://creativecommons.org/licenses/by-sa/4.0/)

from random import random, seed, choice, uniform
from math import floor, ceil
import numpy as np
from PIL import Image

class Vec2:
	#A 2d Vector
	def __init__(self, x, y):
		self.x = x
		self.y = y

def print_monster(monster):
	#Cmd line render of the monster
	for i in range(monster.shape[0]):
		for j in range(monster.shape[1]):
			if np.any(monster[i, j] > 0):
				print("*", end="")
			else:
				print(" ", end="")
		print()

def fmap(a_rge, b_rge, c):
	#maps C from range A0-A1 to range B0-B1
    return b_rge.x + (c - a_rge.x) * (b_rge.y - b_rge.x) / (a_rge.y - a_rge.x)

def make_monster(mon_seed, size, loc):
	'''
	Construct an pixmon 
		args ->
			mon_seed : the seed for the random number generator
			size : width & height of the numpy array
			loc : Vector of top center point of the pixmon
		returns -> 
			A 3d numpy array of u8s with shape [width][height][rgb]
	'''

	#initialise the rng
	seed(mon_seed)

	#Blank Datastructure for our monster
	monster = np.zeros((size, size, 3), dtype=np.uint8)

	#Chance to flip axis for a bit more variety
	flip_axis = choice([True, False])

	#width and height of draw space
	w = size - 3 if flip_axis else size/2 - 1
	h = size - 3 if not flip_axis else size/2 - 1

	#Generate sensible biases... just the right amount of chaos
	normal = Vec2(0.0, 1.0)
	density = fmap(normal, Vec2(0.8, 0.9), random())
	y_bias = fmap(normal, Vec2(0.2, -0.2), random())
	colour_rand = fmap(normal, Vec2(0.75, 0.95), random())

	for k in range(0, int(w*h)):
		#We need our globals, depending on if we flipped
		i = k/w if flip_axis else k % w
		j = k/w if not flip_axis else k % w

		#Let's put our biases to work
		is_hole = random() > density
		a_scalar = floor(uniform(0, size/2)) * floor(uniform(0, size/2))
		y_scalar = floor(1.0 - 2.0 * y_bias)
		xy_scalar = pow(i, 2) + pow(j - y_scalar * (h/2), 2)
		bias = a_scalar > xy_scalar

		#If we're in the sweet spot
		if bias and not is_hole:

			# Indices
			pxl = floor(loc.x - int(i)) % size #Prime X axis
			pxr = floor(loc.x + int(i)) % size #Mirror X axis
			py = floor(loc.y + int(j)) % size #Row

			#Get the colour based on colour_rand (which is based on mon_seed)
			colour = np.random.randint(0, int(255 * colour_rand), size=(3,)) 

			#Paint it symetrically
			monster[pxl][py] = colour
			monster[pxr][py] = colour

	#Looks better 90deg right
	monster = np.rot90(monster, k=-1, axes=(0, 1))

	return monster


############################################
import pygame
from time import sleep

# Simulator Constants
GRID_SIZE = 16  # Number of rows and columns in the matrix
PIXEL_SIZE = 64  # Size of each pixel
CELL_PADDING = 4  # Padding between displayed cells
WINDOW_SIZE = GRID_SIZE * (PIXEL_SIZE + CELL_PADDING) - CELL_PADDING

# Pygame Initialization
pygame.init()
screen = pygame.display.set_mode((WINDOW_SIZE, WINDOW_SIZE))
pygame.display.set_caption('Monster Simulator')
clock = pygame.time.Clock()

def convert_to_simulator_format(monster):
    # Converts the monster numpy array into simulator-compatible format:
    # - glyph_code: Binary array (1D list) indicating whether a pixel is on.
    # - target_colors: RGB values for each pixel in the glyph.
    glyph_code = []
    target_colors = []
    for row in monster:
        for pixel in row:
            is_on = np.any(pixel > 0)  # Check if the pixel has any colour
            glyph_code.append(1 if is_on else 0)  # Append binary on/off value
            target_colors.append(tuple(pixel) if is_on else (0, 0, 0))  # Append RGB value or black
    return glyph_code, target_colors

def display_glyph_on_screen(glyph_code, colors):
    #Displays the glyph on the Pygame screen using the simulator format.
    screen.fill((0, 0, 0))  # Clear the screen
    for i, is_on in enumerate(glyph_code):
        row, col = divmod(i, GRID_SIZE)
        x_start = col * (PIXEL_SIZE + CELL_PADDING)
        y_start = row * (PIXEL_SIZE + CELL_PADDING)
        color = colors[i] if is_on else (0, 0, 0)
        pygame.draw.rect(screen, color, (x_start, y_start, PIXEL_SIZE, PIXEL_SIZE))
    pygame.display.flip()

if __name__ == '__main__':
    # Monster Parameters
    size = GRID_SIZE
    seed_val = 81
    location = Vec2(int(size / 2), 3)

    # Make a Monster
    monster = make_monster(seed_val, size, location)

    # Convert to Simulator Format
    glyph_code, target_colors = convert_to_simulator_format(monster)

    # Display Monster in Simulator
    display_glyph_on_screen(glyph_code, target_colors)
    sleep(5)  # Display for 5 seconds

    pygame.quit()
3 Likes

:partying_face:

Looks great mark.

I personally feel option 2 is more fun.
We can ditch numpy and just build a custom micropython data-structure to hold our data.

Leave it with me; I have some thoughts

3 Likes

Awesome.

Also good to note that as the matrix is 16x16, an even number, a single line down the centre is actually a bit to the left or a bit to the right. So to display with perfect symmetry centre lines need to be doubled.

Or we can just leave out the last column making it 15 cols x 16 rows, but it will always look offset with the current diffuser. I can always print another with a ‘frame’ that makes it look centred.

2 Likes

Last night I found a numpy-less solution.

The only libraries you need is math and random, and they both have micropython ports.

from random import random, seed, choice, uniform
from math import floor, ceil

To replace the functionality of the numpy array I made this Pixamon object. It’s really just a 3d array with some getters, setters, validators, and helper methods.

class Pixamon:

	def __init__(self, size):
		self._size = size
		self.data = self.get_blank_data()

	def get_blank_data(self):
		return [[[0, 0, 0] for _ in range(self._size)] for _ in range(self._size)] 

	def set(self, x, y, rgb):
		if not self.valid_colour(rgb):
			raise NotImplementedError("RGB data must be 3*collection of uInt8")
		if not self.valid_coords(x, y):
			raise ArgumentException("Invalid Co-ordinates")
		self.data[x][y] = list(rgb)

	def get_rgb(self, x, y):
		if not self.valid_coords(x, y):
			raise ArgumentException("Invalid Co-ordinates")
		return self.data[x][y]

	def is_colour_at(self, x, y):
		return any(self.data[x][y])

	def get_rotated90(self):
		rotated_data = self.get_blank_data()
		for i in range(self._size):
		    for j in range(self._size):
		    	rotated_data[j][self._size - 1 - i] = self.data[i][j]
		return rotated_data

	def valid_coords(self, x, y):
		return not (0 < x < self._size & 0 < y < self._size)

	def valid_colour(self, rgb):
		is_valid = (len(rgb) == 3)
		print(len(rgb) == 3)
		for c in rgb:
			print(c)
			print(isinstance(c, int))
			print(c in range(0, 256))
			is_valid &= isinstance(c, int)
			is_valid &= c in range(0, 256)
		return is_valid

	def draw_to_console(self):
		rota = self.get_rotated90()
		for i in range(len(rota)):
			for j in range(len(rota[i])):
				if any(rota[i][j]):
					print("*", end="")
				else:
					print(" ", end="")
			print()

We need to modify the make_monster() function to use our new data-type.

class Vec2:
	#A 2d Vector
	def __init__(self, x, y):
		self.x = x
		self.y = y

def make_random_colour():
	r = floor(random() * 255)
	g = floor(random() * 255)
	b = floor(random() * 255)
	return [r,g,b]

def fmap(a_rge, b_rge, c):
	#maps C from range A0-A1 to range B0-B1
    return b_rge.x + (c - a_rge.x) * (b_rge.y - b_rge.x) / (a_rge.y - a_rge.x)

def make_monster(mon_seed, size, loc):
	'''
	Construct an pixmon 
		args ->
			mon_seed : the seed for the random number generator
			size : width & height of the numpy array
			loc : Vector of top center point of the pixmon
		returns -> 
			A 3d numpy array of u8s with shape [width][height][rgb]
	'''

	#initialise the rng
	seed(mon_seed)

	#Blank Datastructure for our monster
	monster = Pixamon(size)

	#Chance to flip axis for a bit more variety
	flip_axis = choice([True, False])

	#width and height of draw space
	w = size - 3 if flip_axis else size/2 - 1
	h = size - 3 if not flip_axis else size/2 - 1

	#Generate sensible biases... just the right amount of chaos
	normal = Vec2(0.0, 1.0)
	density = fmap(normal, Vec2(0.8, 0.9), random())
	y_bias = fmap(normal, Vec2(0.2, -0.2), random())
	colour_rand = fmap(normal, Vec2(0.75, 0.95), random())

	for k in range(0, int(w*h)):
		#We need our globals, depending on if we flipped
		i = k/w if flip_axis else k % w
		j = k/w if not flip_axis else k % w

		#Let's put our biases to work
		is_hole = random() > density
		a_scalar = floor(uniform(0, size/2)) * floor(uniform(0, size/2))
		y_scalar = floor(1.0 - 2.0 * y_bias)
		xy_scalar = pow(i, 2) + pow(j - y_scalar * (h/2), 2)
		bias = a_scalar > xy_scalar

		#If we're in the sweet spot
		if bias and not is_hole:

			# Indices
			pxl = floor(loc.x - int(i)) % size #Prime X axis
			pxr = floor(loc.x + int(i)) % size #Mirror X axis
			py = floor(loc.y + int(j)) % size #Row

			#Get the colour based on colour_rand (which is based on mon_seed)
			colour = make_random_colour()

			#Paint it symetrically
			monster.set(pxl, py, colour)
			monster.set(pxr, py, colour)

	return monster

And now make_monster() returns a Pixamon, not a numpy array.

#Make a Monster
my_pixamon = make_monster(seed_val, size, location)
my_pixamon.draw_to_console()

Should be able to run this in micropython for LEDS

1 Like