Mini Martial Artists

Ah the wonderful world of non-fungible tokens (NFTs). The idea of NFTs are this: 


You get your “ID” tied to something else’s “ID” on “the blockchain”. That way even if someone else copies and takes that thing you own on the blockchain it’s still actually yours because the all-knowing blockchain record says so.


Clear as mud right? The tech does have some interesting implications and I’m sure it’s more complicated than my simplified breakdown of it (NFT diehards please don’t make me into an NFT genie and trap me in the blockchain for all eternity). However I’m more interested in the idea of procedurally generated art than the NFT tech itself. Like I’m sure many other programmers did when they heard about this craze I decided I to give it a shot. I’ve been getting really into UFC fighting lately and thought it might be fun to randomly generate tiny profile-picture sized fighters (90x90 pixels). Unfortunately for me, I’m terrible at drawing and wouldn’t know where to start with this kind of thing. Fortunately for my cousin who can make digital art, I was willing to pay for someone else to put in the effort. 


We started out by thinking about creating layers of images. We could create a Python script that layered different cosmetics onto a base image to generate unique fighters. My cousin sent me the base image to start out:

I decided to create a folder structure that would help us indicate where a given cosmetic would belong on the screen. I’m calling the base image of the fighters “0 chars”; 0 so the folder remained at the top of the list and “chars” representing different characters. I decided “body” would be better suited for things the fighter could wear on their torso / chest: 
 

 

 

Before combining these components together I wanted to make sure that they would fit. My first step was to programatically check and make sure that every file we used was 90x90 pixels. That way we wouldn’t need to worry about positioning different cosmetics. Wherever my cousin placed the cosmetic in the 90x90 picture (with a blank background) was where it would end up in relation to the fighter. Thankfully Python’s Pillow (PIL) module can be used to read an image’s resolution as well as adjust and edit the different images:

from PIL import Image
import os

path = r"./Avatars"

# For every type of cosmetic in each of their separate folders,
# check and see if the file resolution is 90 x 90
for cosmetic_type in os.listdir(path):
    if '_' not in cosmetic_type and '.' not in cosmetic_type:
        cosmetic_path = path + f'/{cosmetic_type}/'
    for img_name in os.listdir(cosmetic_path):
    # loading the image, make sure we’re looking at png files
        if img_name[-3:] == 'png':
            print(img_name)
            img = Image.open(f"{cosmetic_path}/{img_name}")
            # fetching the dimensions
            wid, hgt = img.size

            if wid != 90 or hgt != 90:
                # displaying the dimensions
                print(f"Incorrect resolution: {img_name} is {wid}x{hgt}")

 

My cousin’s base image of our fighter was a little bit “spicy” if you catch my drift so he sent me over this file of mini shorts to increase his defense:


Which was surprisingly easy to put together using Pillow:

# Open our individual image components
base_character = Image.open(r"./Avatars/0 chars/common/basic_male.png")
shorts = Image.open(r"./ Avatars/shorts/common/baby blue shorts.png")
# Combine them together
combined_character = Image.alpha_composite(base_character, shorts)
# Save the resulting image
combined_character.save(r'./Avatars/_test_characters/not_naked_fighter.png')

 

Behold, the unstoppable force that is our non-naked fighter:

Pillow also has a color mapping feature which can give off some pretty neat effects and really make a fighter’s style come together. Since we’re using pixel art and keeping the number of colors to a minimum we can find the unique colors used in the component and transform them to be whatever else we want them to be. This is a little trickier. First we have have to open the image we’re interested in and convert it to RGBA (Red Blue Green Alpha). Then we have to get the data that represents that image. In this case, it would be a 90x90 array of pixels with each pixel’s RGB color

shorts = Image.open(r"./Avatars/shorts/common/baby blue shorts.png")
shorts = shorts.convert("RGBA")
pixel_grid = shorts.getdata()

 

Then, we can find all of the unique colors within that pixel grid. In this case we’ll store each rgb code as a string with “R_G_B” and take the set of all pixel colors to get the number of unique colors we’re working with for these shorts.

# Find all the different RGB colors used in this item
all_colors = [f"{r}_{g}_{b}" for r,g,b,a in pixel_grid if a==255]
unique_colors = set(all_colors)
unique_colors
[O]: {'105_145_188', '105_145_206', '149_189_236'}

 

Here we can see we have 3 primary colors for our shorts. If we want to figure out which is of these is our base color (the color most dominant in the image) we simply count the number of pixels each color takes in our image. 

# Find the color that appears the most often (the base color)
color_frequency = defaultdict(lambda: 0)
for color in all_colors: color_frequency[color] += 1
color_frequency
[O]: {'105_145_188': 27, '105_145_206': 244, '149_189_236': 449}

 

This means that , '149_189_236' which represents (R,G,B) (149, 189, 236) is our base color because it occurs the most frequently (in 449 pixels). We can take the max of the color_frequency values and convert it back to integers to get our base RGB color:

# Extract our base color and convert it back to integers
base_color = max(color_frequency, key=color_frequency.get)
rbase, gbase, bbase = [int(c) for c in base_color.split('_')]


We can use this base color to retain a similar style for our shorts (ex. so the shading in this case remains darker than the base color). To do this, we’re more interested in how our other colors in the image change relative to our base color. To get this difference, we’ll subtract the base color’s red, green, and blue values from our other unique colors:

# Find the difference between the base color and every other color
# by RGB value. This will keep our other colors shades and colors
# relative to the primary color of the cosmetic
color_map = {}
for color in all_colors:
    r, g, b = [int(c) for c in color.split('_')]
    color_map[color] = (r-rbase, g-gbase, b-bbase)


This gives us a dictionary that tells us how we want to adjust other colors relative to the base color. Now all that’s left to do is for us to set the base color to whatever we want and change the colors of those pixels:
 

# Select which RGB color we want our new base color to be. 
# We’ll set our base color to red for this example.
color_map[base_color] = (255, 0, 0)
# Adjust all other colors in the image around this new
# base color
for color in color_map.keys():
    if color != base_color:
        new_base_r, new_base_g, new_base_b = color_map[base_color]
        r_adjustment, g_adjustment, b_adjustment = color_map[color]
        
        color_map[color] = (
                            new_base_r + r_adjustment,
                            new_base_g + g_adjustment,
                            new_base_b + b_adjustment,
                            )
color_map
[O]: {'105_145_188': (466, -44, -48), '105_145_206': (466, -44, -30), '149_189_236': (255, 0, 0)}

RGB values are only supposed to be between 0 and 255 so these negative values and values over 255 don’t technically make sense. Thankfully Pillow does it’s magic (probably by using a floor of 0 and ceiling of 255 so that colors stay within those bounds) and we don’t break the code by using illegal values. However if I did need to change this, there is an interesting Stack Overflow Post about using mod math to make sure it stays within bounds.


Finally we’ll replace each pixel in the image based on our new color mapping. We won’t change white or black and only care about pixels whose alpha (opacity) value is greater than or equal to 100.   

# Adjust each pixel in the image to map to the new
# base color
newData = []
for pixel in pixel_grid:
    r,g,b,a = pixel
    color_key = f"{r}_{g}_{b}"
    # Main Body
    if (r == 0 or r == 255) and (g == 0 or g == 255) and (b == 0 or b == 255):
        newData.append(pixel)
    elif a>=100:
        new_r, new_g, new_b = color_map[color_key]
        newData.append((new_r, new_g, new_b, a))
    else:
        newData.append(pixel)

# Adjust the colors of the shorts object based on our new mapping
shorts.putdata(newData)

Behold, our brand new TOMATO RED shorts:


Which our fit our fighter perfectly:

combined_character = Image.alpha_composite(base_character, shorts)

 

Thanks to the modular way we designed our code, we can make our shorts as blue, green, or red as we like. My favorite thing to do is to set the base color to random, produce a bunch, and find which ones look coolest to me. The RGB color difference mechanism can give us some really neat designs:


We can apply this re-coloring to any of our images and future cosmetics to mix and match them, including our base character who now comes in all different colors from reasonable to radioactive:


There is plenty more to discuss when it comes to item rarity, creating sets, etc. however this post already covers the core concepts. At this point it’s simply a matter of creating different cosmetics and trying them out:

 

I call them my Mini Martial Artists and I love each and every one of them. I have plans for how I want to use these little guys so stay tuned to see where they end up next.

Previous Post Hashy Birthday! Next Post Sports Betting with Python - Part 1 Project Introduction
Sports Betting with Python - Part 1 Project Introduction
Sept. 4, 2022, 3 a.m.
Spending Waaaaaaay Too Much Time at Bars
Jan. 20, 2024, 9:02 p.m.
Hashy Birthday!
Sept. 4, 2022, 2:57 a.m.