I recently purchased an NFT from the Cryptopunks Reload collection. These animations re-create a Cryptopunk by filling in each of the features of a punk one pixel at a time. I was excited to get a hold of one of these and equally excited to try to figure out how they made them. Go check them out!
In this post, I’ll use the cpunks-10k Python API and the numpy library to write a generator that makes something fairly close to the Reloaded punks.
The CPUNX-10k API can be installed into a virtual environment using pip.
$ pip install cpunx-10k
After installation, you can work with the dataset in a Jupyter notebook much like you may work with the MNIST dataset available in the Tensorflow API.
import cpunks10k
import cpunks10k.utils as cpu
cp = cpunks10k.cpunks10k()
(X_train, Y_train), (X_test, Y_test), (labels) = cp.load_data()
The CPUNX-10k dataset is a set of 10,000 (24,24,4) images encoded in four RGBT channels. In this post, I’m only going to look at a single punk, and I won’t be doing any model training, but I still like to load all punks into train, test and labels to be consistent. To check that loading worked properly, load a single punk and display it in the notebook.
plt.imshow(X[4534])
The cpunks-10k punks are stored as 24x24x4 arrays with the 3rd dimension representing a 4 channel color used in the punks color palette. For this exercise, I found it useful to reduce the problem to one dimension, by first, encoding the color as a string and flattening the image to a 1d 576 element numpy array.
img = X[4534]
img_flat = cpu.flatten(img).reshape((24*24))
To generate the frames of the animation, I’ll use a 1d mask of 576 boolean values which will determine whether or not a pixel is ready to be shown. The mask will start out as all zeros and I’ll add pixels for each color layer in a series of loops.
mask = np.zeros((24*24), dtype=np.uint8)
To identify each layer, I’ll need to know the set of unique colors used in this Cryptopunk and ideally, be able to call them by a name.
black = '[0. 0. 0. 1.]'
skin_color = '[0.85882354 0.69411767 0.5019608 1. ]'
np.unique(img_flat)
array(['[0. 0. 0. 0.]', '[0. 0. 0. 1.]',
'[0.5019608 0.85882354 0.85490197 1. ]',
'[0.5764706 0.21568628 0.03529412 1. ]',
'[0.6509804 0.43137255 0.17254902 1. ]',
'[0.7921569 0.30588236 0.06666667 1. ]',
'[0.85882354 0.69411767 0.5019608 1. ]'], dtype='<U45')
The notebook code got complicated quickly so I took a pass at abstracting the core logic for frame generation into two functions.
def get_frame(img, mask, default = '[0. 0. 0. 0.]'):
'''given an img and an image mask, return the masked image using `default` for the background color`
frame = np.array([img[i] if mask[i] else default for i in range(0,(24*24))]).reshape(24,24)
return cpu.unflatten(frame)
def frames_for_color(img, mask, color, num_to_pop, default = '[0. 0. 0. 0.]'):
'''given an image flattened with cpunks-10k utils,
a mask, and a color (or layer), create the N images
for this layer animation
'''
imgs = []
idx = np.where(img == color)[0]
while True:
np.random.shuffle(idx)
n, rest = idx[-num_to_pop:], idx[:-num_to_pop]
# update the mask
np.put(mask, n, np.ones(len(n), dtype=np.int8))
imgs.append(get_frame(img, mask, default))
if len(rest) <= 0:
break
else:
idx = rest
return (imgs, mask)
Finally, use the two functions above to create the animation for punk 5076.
default = '[0.5019608 0.85882354 0.85490197 1. ]'
(imgs_1, mask) = frames_for_color(img_flat, mask, skin_color, 3, default=default)
(imgs_2, mask) = frames_for_color(img_flat, mask, black, 3, default=default)
imgs = imgs_1 + imgs_2
unique = [
'[0.5764706 0.21568628 0.03529412 1. ]',
'[0.7921569 0.30588236 0.06666667 1. ]',
'[0.6509804 0.43137255 0.17254902 1. ]',
'[0.5019608 0.85882354 0.85490197 1. ]']
for color in unique:
(imgs_new, mask) = frames_for_color(img_flat, mask, color, 2, default=default)
imgs = imgs + imgs_new
Matplotlib has great features for displaying and saving animated gifs. The code below takes the images array generated above, saves it to disk and displays the animation in a separate window.
import matplotlib.animation as animation
%matplotlib qt
imagelist=imgs
fig = plt.figure()
im = plt.imshow(imagelist[0])
def updatefig(j):
# set the data in the axesimage object
im.set_array(imagelist[j])
return [im]
# kick off the animation
ani = animation.FuncAnimation(fig, updatefig, frames=range(len(imagelist)),
interval=96)
writer = animation.PillowWriter(fps=25)
ani.save("../tmp/4534_fade_v3.gif", writer=writer)
plt.show()
The mission of Colorpunx is to use the Cryptopunks as a dataset for teaching and understanding generative art using data science and machine learning approaches. I hope this tutorial will help somebody understand how a 10,000 NFT set may be generated using very basic math and a little numpy matrix manipulation. The full example is available on Github at FadetoPunk.