Yesterday in our SPb Python telegram chat a question "how to automatically cut spritesheet into several sprites" was asked. An example of spritesheet image is here: http://www.spriters-resource.com/3ds/animalcrossinghappyhomedesigner/sheet/68350/
Several solutions were proposed, and today I've decided to write my own solution.
So the problem is to cut each sprite out and save to separate file. As a parameter could be passed the color of border. By the way by default it could be fetched from top left pixel. I've coded a Python script that takes input spritesheet image, border color if known, and output folder to save found and cutted out sprites. Output sprite images are grouped by size.
Several solutions were proposed, and today I've decided to write my own solution.
So the problem is to cut each sprite out and save to separate file. As a parameter could be passed the color of border. By the way by default it could be fetched from top left pixel. I've coded a Python script that takes input spritesheet image, border color if known, and output folder to save found and cutted out sprites. Output sprite images are grouped by size.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
#-*- coding: utf-8 -*- | |
#----------------------------------------------------------------------- | |
# Author: delimitry | |
#----------------------------------------------------------------------- | |
import os | |
import sys | |
from PIL import Image | |
from argparse import ArgumentParser | |
def find_horizontal_borders(image, border_color): | |
""" | |
Find horizontal border lines with defined color | |
""" | |
w, h = image.size | |
pixels = image.load() | |
horizontals = [] | |
for j in xrange(h): | |
for i in xrange(w): | |
if pixels[i, j] != border_color: | |
break | |
else: | |
horizontals.append(j) | |
return horizontals | |
def find_vertial_borders(image, border_color): | |
""" | |
Find vertical border lines with defined color | |
""" | |
w, h = image.size | |
pixels = image.load() | |
verticals = [] | |
for i in xrange(w): | |
for j in xrange(h): | |
if pixels[i, j] != border_color: | |
break | |
else: | |
verticals.append(i) | |
return verticals | |
def find_areas(image, border_color): | |
""" | |
Find sprite areas and return a list of rectangles (x, y, w, h). | |
""" | |
w, h = image.size | |
horizontals = find_horizontal_borders(image, border_color) | |
areas = [] | |
prev_horizontal = 0 | |
for h in horizontals: | |
if h - prev_horizontal > 1: | |
verticals = find_vertial_borders( | |
image.crop((0, prev_horizontal + 1, w, prev_horizontal + (h - prev_horizontal))), border_color) | |
prev_vertical = 0 | |
for v in verticals: | |
if v - prev_vertical > 1: | |
areas.append( | |
(prev_vertical + 1, prev_horizontal + 1, v - prev_vertical - 1, h - prev_horizontal - 1)) | |
prev_vertical = v | |
prev_horizontal = h | |
return areas | |
def str_to_color(value): | |
""" | |
Convert str value to color (tuple of ints) | |
""" | |
try: | |
if ',' not in value or value.count(',') not in (2, 3): | |
raise | |
color = tuple([int(x) for x in value.split(',')]) | |
for c in color: | |
if not 0 <= c <= 255: | |
raise | |
return color | |
except: | |
raise Exception('Invalid format of color. Must be (R,G,B) or (R,G,B,A), ' | |
'where RGBA channels must be in [0..255]') | |
def cut_and_save_sprites(image, areas, output_folder): | |
""" | |
Cut sprites from image using found areas and save to output folder, grouping them by size | |
""" | |
if not os.path.exists(output_folder): | |
os.makedirs(output_folder) | |
if not areas: | |
return | |
group = 0 | |
index = 0 | |
# get [w, h] from first area (x, y, w, h) | |
prev_size = areas[0][2:4] | |
for area in areas: | |
# assume that a new group starts when a new size is found | |
if area[2:4] != prev_size: | |
prev_size = area[2:4] | |
group += 1 | |
index = 0 | |
image.crop((area[0], area[1], area[0] + area[2] - 0, area[1] + area[3] - 0)).save( | |
os.path.join(output_folder, 'group_%02d_index_%03d.png' % (group, index))) | |
index += 1 | |
def main(): | |
# prepare arguments parser | |
parser = ArgumentParser(usage='%(prog)s [options]', description='Spritesheet image cutter') | |
parser.add_argument('-i', '--input', dest='input', help='spritesheet image') | |
parser.add_argument('-c', '--color', dest='color', | |
help='border RGB/RGBA color (e.g.: 255,0,128 or 0,0,0,255), by default color of top left pixel', default='') | |
parser.add_argument('-o', '--output', dest='output', | |
help='output sprites folder, by default current folder', default='./') | |
parser.add_argument('-v', '--version', action='version', version='Spritesheet image cutter v0.1') | |
args = parser.parse_args() | |
if not args.input: | |
parser.print_help() | |
sys.exit() | |
image = Image.open(args.input) | |
left_top_color = image.getpixel((0, 0)) | |
if args.color: | |
try: | |
border_color = str_to_color(args.color) | |
except Exception as ex: | |
sys.exit(ex) | |
else: | |
# get top left pixel color if not color passed | |
border_color = left_top_color | |
# check color channels | |
if len(border_color) != len(left_top_color): | |
print 'Warning: Image color channels number is %d, but you have passed %d.' % ( | |
len(left_top_color), len(border_color)) | |
sys.exit() | |
areas = find_areas(image, border_color) | |
if not areas: | |
print 'No sprites were found.' | |
sys.exit() | |
print 'Saving %s sprites to "%s"' % (len(areas), os.path.abspath(args.output)) | |
cut_and_save_sprites(image, areas, args.output) | |
if __name__ == '__main__': | |
main() |
No comments:
Post a Comment