Fade into You

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!

Cryptopunks Reload #0002
Cryptopunks Reload #0002

How did they do that?

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.

Getting Setup

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])
Cryptopunk #4534
Cryptopunk #4534

Taking things down to size

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))

The Basic Approach

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')

Helper Functions

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

Displaying and Saving the Animation

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()
Bootleg Punk Reloaded
Bootleg Punk Reloaded

Fade Out

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.

Subscribe to 0x9C65…e51c
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.