• 📚 Admin Project Update: I've added a major feature to PictureBooks.io called Avatar Studio! You can now upload photos to instantly turn your kids (and pets! 🐶) into illustrated characters that star in their own stories. Give it a try and let me know what you think!

Storyboard Palette Python script???

Stew Pid

Warlord
Joined
Apr 29, 2018
Messages
232
Location
Manhatten
Hello fellow Civ 3 modders,

I asked Gemini 3 if it could in some way help with civ 3 unit creation out of curiosity.

It complied info from these forums and spat out a python script that it says should compliment storyboard and palette creation.

Can a python expert and unit maker chime in here and see if this script is actually useful?

import sys
from PIL import Image, ImagePalette

def create_civ3_palette_image(input_image_path, output_image_path):
"""
Converts an image to a valid Civ 3 Indexed PCX/BMP.
- Slots 0-31: Reserved for Team Color (Blue gradient placeholder).
- Slots 32-253: Optimized palette from the input image.
- Slot 254: Green (Shadow).
- Slot 255: Magenta (Transparency).
"""

# 1. Load the user's image (Must be RGB mode)
img = Image.open(input_image_path).convert("RGB")
width, height = img.size
pixels = img.load()

# 2. Define Reserved Colors
TRANS_MAGENTA = (255, 0, 255) # Index 255
SHADOW_GREEN = (0, 255, 0) # Index 254

# Generate Team Color Gradient (Blue) for indices 0-31
# Civ3 uses this blue ramp as a placeholder for the civ-specific colors.
team_colors = []
for i in range(32):
# Create a gradient from Dark Blue to Light Blue
# This is a safe approximation of the standard Civ3 donor palette
val = int((i / 31) * 255)
team_colors.append((0, 0, val))

# 3. Quantize the "Body" of the unit
# We need 222 colors (256 total - 32 team - 1 shadow - 1 trans)
# We exclude Magenta and Green from this quantization to strictly reserve them.

# Trick: Crop out transparent areas if possible, or just quantize the whole thing
# then force-fix the reserved colors later.
quantized = img.quantize(colors=222, method=2) # Method 2 is Fast Octree
body_palette = quantized.getpalette()[:222*3] # Get the flat RGB list

# Convert flat list to tuples
body_colors = []
for i in range(0, len(body_palette), 3):
body_colors.append((body_palette, body_palette[i+1], body_palette[i+2]))

# Fill remaining slots if image had fewer than 222 colors
while len(body_colors) < 222:
body_colors.append((0, 0, 0))

# 4. Construct the Final Master Palette (List of 256 tuples)
# 0-31: Team Colors
# 32-253: Body Colors
# 254: Green
# 255: Magenta
final_palette_rgb = []
final_palette_rgb.extend(team_colors) # 0-31
final_palette_rgb.extend(body_colors) # 32-253
final_palette_rgb.append(SHADOW_GREEN) # 254
final_palette_rgb.append(TRANS_MAGENTA) # 255

# Flatten for PIL
flat_palette = [c for color in final_palette_rgb for c in color]

# 5. Create the New Image
# We cannot just "convert" because we need to force specific indices.
# We create a new Paletted image and paste the data.

# Helper: Find nearest color index in our new specific palette
def get_nearest_index(pixel):
r, g, b = pixel

# STRICT RESERVED CHECKS
if pixel == TRANS_MAGENTA: return 255
if pixel == SHADOW_GREEN: return 254

# For body colors, find closest match in indices 32-253
# (We skip 0-31 because we don't want the unit body to accidentally flash team colors)
best_idx = 32
min_dist = float('inf')

for idx in range(32, 254):
pr, pg, pb = final_palette_rgb[idx]
dist = (r-pr)**2 + (g-pg)**2 + (b-pb)**2
if dist < min_dist:
min_dist = dist
best_idx = idx
return best_idx

# Create blank indexed image
output_img = Image.new("P", (width, height))
output_img.putpalette(flat_palette)

# 6. Remap pixels (This can be slow for massive images, but fine for unit strips)
# For speed, we can use a cache
print("Remapping pixels... this may take a moment.")
new_data = []
cache = {}

for y in range(height):
for x in range(width):
p = pixels[x, y]
if p not in cache:
cache[p] = get_nearest_index(p)
new_data.append(cache[p])

output_img.putdata(new_data)

# 7. Save
output_img.save(output_image_path)
print(f"Success! Saved to {output_image_path}")
print("Remember: Indices 0-31 are BLUE Team Colors.")
print("If you want parts of your unit to change color, paint them BLUE in your editor now.")

# --- USAGE ---
# Run this from terminal or command prompt:
# python civ3_palette_maker.py
if __name__ == "__main__":
# Example: Change these filenames to what you need
create_civ3_palette_image("Storyboard.bmp", "Storyboard_Indexed.pcx")
 
Disclaimer: I know Python a lot better than I know graphics, I've never used PIL, and it's been a LONG time since I've made a unit animation, but...

ehhh, it seems to have mostly gotten the point but I wouldn't trust it as is. IIRC there's a whole other range of indices used for smoke and shadow, not 254 which is also transparent. It seems to be assuming that all blues and only blues will map to the nearest civ-color index, but I can imagine that might not hold true for certain color distributions. And since it quantizes the whole image down to 222 colors, it seems like that's already going to include all of your civ-colors so even if it ends up correctly mapping those to the first 31 indices, there will be wasted redundant colors in the palette. I think a better approach would be to have a first pass to detect which colors are "blue", quantize that selection to 31 colors, and sort those to the beginning. I don't really remember how this is traditionally done though so I may be off base here.
 
Back
Top Bottom