According to Weimer

Image Extraction from Sprite Sheets

Introduction

My middle school students needed a quick way to extract sprites from sprite sheets to use in their Pygame Zero projects.

Code

I made a simple GUI allowing students to easily extract sprites from a spritesheet. Each of the sprites need to be the same size.

With the Pygame Zero Helper library, the students can now easily add animations to their projects.

I encourage the students to make the code more user friendly and add any features they think might be helpful.

Python File Download
import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageDraw, ImageTk
from os import mkdir, path

filename = "No File Selected"


def choose_file():
    global filename
    filename = filedialog.askopenfilename(
        filetypes=[
            ('PNG files', '*.png *.PNG')
        ]
    )
    file_selected['text'] = path.basename(filename)
    show_grid(filename)


def extract_images(file):
    if file != "No File Selected":
        sheet = Image.open(file)
        image_prefix = prefix.get()
        size = scale.get()
        directory = "extracted sprites"

        if not path.exists(f"{path.dirname(file)}/{directory}"):
            mkdir(f"{path.dirname(file)}/{directory}")

        count = 1
        width, height = sheet.size

        for y in range(0, height + 1, size):
            for x in range(0, width + 1, size):
                image = sheet.crop((x, y, x+size, y+size))
                alpha_range = image.getextrema()[-1]
                if alpha_range != (0, 0):
                    image.save(
                        f"{path.dirname(file)}/{directory}/{image_prefix}_{str(count).zfill(3)}.png")
                    count += 1
        status_label['text'] = "Done!"


def show_grid(file):
    if file != "No File Selected":
        sheet = Image.open(file)
        width, height = sheet.size
        step = scale.get()
        draw = ImageDraw.Draw(sheet)
        for y in range(0, height + 1, step):
            draw.line((0, y, width, y), fill=(255, 0, 0), width=1)
        draw.line((0, height-1, width, height-1), fill=(255, 0, 0), width=1)
        for x in range(0, width + 1, step):
            draw.line((x, 0, x, height), fill=(255, 0, 0), width=1)
        draw.line((width-1, 0, width-1, height), fill=(255, 0, 0), width=1)
        sprite_images = ImageTk.PhotoImage(sheet)
        spritesheet.config(image=sprite_images)
        spritesheet.image = sprite_images

root = tk.Tk()
root.title('Sprite Image Extraction')
root.geometry('+50+50')

controls = tk.Frame(root, borderwidth=1, relief='solid')
controls.grid(row=1, column=2, padx=(20, 20), pady=(0, 20), sticky=tk.E)

scale = tk.IntVar(value=32)
prefix = tk.StringVar(value="sprite")

title = tk.Label(root,
                    text="Select Spritesheet",
                    font="Arial 16 bold",)
title.grid(row=0, column=0, columnspan=3, pady=(10, 20))

file_selected = tk.Label(controls,
                            text="No Sprite Sheet Selected",
                            font="Arial 11 normal",
                            fg='black')
file_selected.grid(row=1, column=0, columnspan=2, pady=(0, 20))

grid_size = tk.Scale(controls,
                        variable=scale,
                        from_=8,
                        to=104,
                        resolution=2,
                        length=300,
                        width=20,
                        orient=tk.HORIZONTAL,
                        tickinterval=8,
                        command=lambda scale:  show_grid(filename))
grid_size.grid(row=5, column=0, columnspan=2, padx=20, pady=(5, 0))

select_file_button = tk.Button(
    controls,
    text="Select File",
    command=choose_file,
    font="Arial 11 normal")
select_file_button.grid(row=0, column=0, columnspan=2,
                        pady=(20, 5), padx=20, sticky=tk.NW)

prefix_label = tk.Label(controls, text="Enter Prefix:",
                        font="Arial 11 normal")
prefix_label.grid(row=2, column=0, padx=(20, 0))

prefix_entry = tk.Entry(controls, width=30, textvariable=prefix,
                        font="Arial 11 normal")
prefix_entry.grid(row=2, column=1, pady=20, padx=(0, 20))

status_label = tk.Label(controls, text="", fg='blue', font="Arial 11 normal")
status_label.grid(row=6, column=0, sticky=tk.SW, padx=20, pady=20)

extract = tk.Button(controls, text="Extract Images",
                    font="Arial 11 normal",
                    command=lambda: extract_images(filename))
extract.grid(row=6, column=1, sticky=tk.SE, columnspan=2, padx=20, pady=20)

spritesheet = tk.Label(root,
                        text="No Sprite Sheet Selected",
                        font=("Arial 11 normal"),
                        fg='red')
spritesheet.grid(row=1, column=0, columnspan=2,
                    rowspan=7, padx=(20, 0), pady=(0, 20))

root.mainloop()
May 01, 2023 Keywords: sprite sheet

Parsons Problems Helper Files

Introduction

I wanted to create some Parsons problems for my students to do, but formatting the examples was kind of tedious. The example code snippets will convert a Python file to the correct format to use in the Parsons problems pages.

Code

This code formats a Python file to the correct format and saves it to a text file. The formatted text can then be used in conjunction with the js-parsons JavaScript library to create a Parsons problem.

The documentation for js-parsons and the source code for my Parsons problems pages will get you started.

If you place a #t at the end of a line in your Python code to be formatted, it will combine it with the next line. See the code below for an example, and check out the Pentagon Parsons problem to see what the final product would look like.

from turtle import *
shape('turtle')#t
n_sides = 5
for i in range(n_sides):
    fd(100)
    rt(360/n_sides)
                    
def convert_parsons(code_in = "text.txt", code_out = "text_out.txt"):
    """ Converts the code_in text to format for js_parsons.
    A line that ends in #t gets combined with the next line
    so that it is part of the same block """
    with open(code_in, 'r') as reader:
        lines = reader.readlines()
        
    with open(code_out, 'w', newline='') as writer:
        for line in lines:
            if line != lines[-1]:
                if line[-3:-1] == "#t":
                    writer.write(f"'{line[:-3]}"+r"\\n' "+'+\n')
                else:
                    writer.write(f"'{line[:-1]}"+r"\n' "+'+\n')
            else:
                writer.write(f"'{line[:-1]}'")
                    
code_in = input("Name of file to be converted? ")
code_out = input("Name of output file? ")
convert_parsons(code_in = code_in, code_out = code_out)
January 7, 2023 Keywords: Parsons problems

Diffusion Limited Aggregation

Introduction

This was done a while ago, but I thought I would share me efforts. This is my attempt at diffusion limited aggregation using Processing in Python mode. Most of my effort went towards trying to make it more efficient.

Code

The Processing sketch will run until you press a key or the drawing comes within 50 pixels of the edge of the canvas. Once the sketch is done running it will be colored in and the results saved to a PNG file. You can uncomment the two lines in the keyPressed function to save your image as an SVG file.

rainbow colored picture of example for diffusion limited aggregation

# add_library('svg')
randomSeed(372)

d = 2
s = 1000
circles = [(s/2.0, s/2.0)]
new = True
direction = 1
min_x, min_y, max_x, max_y = s/2.0 - d, s/2.0 - d, s/2.0 + d, s/2.0 + d
circs = []


def setup():    
    size(s, s)    
    frameRate(1200)    
    circle(circles[0][0], circles[0][1], d)  


def draw():
    global x, y, circles, new, direction, min_x, min_y, max_x, max_y, circs
    if min_x > 50 and min_y > 50 and max_y < height - 50 and max_x < width - 50:
        if new:
            direction = int(random(1,5))
            if direction == 1:  # top
                x = random(min_x, max_x)
                circs = [circ for circ in circles[::-1] if (x-d) < circ[0]  < (x+d)]
                min_ys = min([circ[1] for circ in circs])-d
                y = min_ys - d
            elif direction == 2:  # bottom
                x = random(min_x, max_x)
                circs = [circ for circ in circles[::-1] if (x-d) < circ[0]  < (x+d)]
                max_ys = max([circ[1] for circ in circs])+d         
                y = max_ys + d
            elif direction == 3: # left                
                y = random(min_y, max_y)
                circs = [circ for circ in circles[::-1] if (y-d) < circ[1]  < (y+d)]
                min_xs = min([circ[0] for circ in circs])-d                
                x = min_xs - d
            elif direction == 4: # right                
                y = random(min_y, max_y)
                circs = [circ for circ in circles[::-1] if (y-d) < circ[1]  < (y+d)]
                max_xs = max([circ[0] for circ in circs])+d
                x = max_xs + d
            new = False
        circle(x, y, d)
        if direction == 1:
            y += 1                   
        elif direction == 2:
            y -= 1
        elif direction == 3:
            x += 1
        elif direction == 4:
            x -= 1
        if y > height + d or y < 0 - d or x > width + d or x < 0 - d:
            new = True 
        for circ in circs:
            if not new:        
                distance = dist(x, y, circ[0], circ[1])
                if distance <= d:
                    circles.append((x,y))
                    min_y = min([circ[1] for circ in circles])-d
                    min_x = min([circ[0] for circ in circles])-d
                    max_y = max([circ[1] for circ in circles])+d
                    max_x = max([circ[0] for circ in circles])+d                    
                    new = True
    else:        
        print("done")
    
                
def keyPressed():
    # beginRecord(SVG, "sand4.svg")
    noLoop()
    colorMode(HSB, 360, 100, 100)
    background(360)
    noStroke()      
    total = len(circles)
    for i,circ in enumerate(circles):
        fill(265*i/(total-1), 100, 100)
        circle(circ[0], circ[1], d)
    # endRecord()
    save("test.png")
January 9, 2023 Keywords: Processing, art