I am writing a Python module which removes the background from animated GIF images. My problem function takes a directory path string and a URL string as input. The function uses an IF statement to pick either the URL or the path, based on if the URL is empty ("") or not. Pseudo-code is basically:
If URL != "", then get URL content and create a PIL.Image, else, search the directory path for gif and use to create PIL.Image.
In either case, a PIL.Image object called "im" is created and then "im" is referenced again after the IF block, which is where I get the error:
UnboundLocalError: local variable 'im' referenced before assignment
I thought that control blocks like IFs, Fors, Whiles, etc. were exempt from the variable scope restrictions that separate function variables from each other, but I am clearly missing something.
Below is my module code. I have put indicator comments around what I believe is the problem area (within function "gif_to_imgs").
src/gif_to_imgs.py
from PIL import Image
import os
import tempfile
import shutil
from rembg import remove
import glob
import contextlib
import requests
from io import BytesIO
def remove_img_bg(img_file, output_path):
output = remove(img_file)
output.save(output_path)
print(f"Background removed, output saved to: \"{output_path}\"")
def imgs_to_video(framesDir):
# https://stackoverflow.com/a/57751793/17312223
fp_in = f"{framesDir}/frame-*.png" # Will return all file paths obeying regex to a list var
fp_out = f"{framesDir}/modified.gif"
# use exit stack to automatically close opened images
with contextlib.ExitStack() as stack:
# lazily load images
imgs = (stack.enter_context(Image.open(f))
for f in sorted(glob.glob(fp_in)))
# extract first image from iterator
img = next(imgs)
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#gif
img.save(fp=fp_out, format='GIF', disposal=2, append_images=imgs,
save_all=True, duration=100, loop=0)
def gif_to_imgs(imgDir,gif_url):
framesDir = f"{imgDir}/gif_frames"
# Delete frames folder if exists
if (os.path.exists(framesDir)):
tmp = tempfile.mktemp(dir=os.path.dirname(framesDir))
shutil.move(framesDir, tmp) # "Move" to temporary folder
shutil.rmtree(tmp) # Delete
os.mkdir(framesDir)
#==============================================================
# PROBLEM BEGINS HERE
#==============================================================
if (gif_url != ""):
response = requests.get(gif_url)
im = Image.open(BytesIO(response.content))
else:
for filename in os.listdir(imgDir):
if (filename == "*.gif"):
im = Image.open(f"{imgDir}/{filename}.gif")
print("Number of frames: " + str(im.n_frames)) #<---- "im" causes the error here
#==============================================================
# CODE FAILS AT THE ABOVE "PRINT()"
#==============================================================
for i in range(0, im.n_frames):
# Create sort-friendly numerical suffix for GIF frames
numSuffix = str(i)
while (len(numSuffix) < 4):
numSuffix = f"0{numSuffix}"
# Remove background from the frame
im.seek(i) # Iterate to specific frame
duration = im.info['duration']/1000 # Get frame duration
output_path = f"{framesDir}/frame-{numSuffix}-{duration}.png" # Set output path
remove_img_bg(im, output_path) # Remove background
imgs_to_video(framesDir)
In case you are curious where the entry point for this module is, see my main.py script that calls it below:
main.py
from src import gif_to_imgs
if __name__ == '__main__':
# gif_url = "https://media.tenor.com/tX_T48A14BwAAAAd/khaby-really.gif"
gif_url = ""
# takes path and url as input
gif_to_imgs.gif_to_imgs("assets", gif_url)
UPDATED – SOLUTION:
I will try to explain how I fixed this. Per @Samwise’s insight that the IF statement would not initialize "im" in all cases, I changed it as follows:
- Added new IF preceding original IF which checks if local gif file exists and assigns to "im2" object if found
- Then, updated original IF to first check URL is not blank, if blank, check if "im2" exists/True, and finally, if neither case is true, it prints an error message and exits the program
Here is the updated code (just the function containing the error spot):
def gif_to_imgs(imgDir,gif_url):
framesDir = f"{imgDir}/gif_frames"
# Delete frames folder if exists
if (os.path.exists(framesDir)):
tmp = tempfile.mktemp(dir=os.path.dirname(framesDir))
shutil.move(framesDir, tmp) # "Move" to temporary folder
shutil.rmtree(tmp) # Delete
# Check if local GIF exists in path
for filename in os.listdir(imgDir):
if filename.lower().endswith(".gif"):
im2 = Image.open(f"{imgDir}/{filename}")
break
else:
im2 = False
os.mkdir(framesDir)
if (gif_url != ""):
response = requests.get(gif_url)
im = Image.open(BytesIO(response.content))
elif im2:
im = im2
else:
print("no URL given and no local GIF file detected - please try again")
sys.exit(1)
print("Number of frames: " + str(im.n_frames))
for i in range(0, im.n_frames):
# Create sort-friendly numerical suffix for GIF frames
numSuffix = str(i)
while (len(numSuffix) < 4):
numSuffix = f"0{numSuffix}"
# Remove background from the frame
im.seek(i) # Iterate to specific frame
duration = im.info['duration']/1000 # Get frame duration
output_path = f"{framesDir}/frame-{numSuffix}-{duration}.png" # Set output path
remove_img_bg(im, output_path) # Remove background
imgs_to_video(framesDir)
>Solution :
This block of code won’t always initialize im
:
if (gif_url != ""):
response = requests.get(gif_url)
im = Image.open(BytesIO(response.content))
else:
for filename in os.listdir(imgDir):
if (filename == "*.gif"):
im = Image.open(f"{imgDir}/{filename}.gif")
Specifically, in the case where there is no gif_url
and there are no files called *.gif
in imgDir
.
Since the Python equality operator doesn’t understand filesystem wildcards, you probably meant for this:
if (filename == "*.gif"):
to be something more like:
if filename.lower().endswith(".gif"):
but you should still probably think about how to handle the case where no files satisfy that condition (or maybe if multiple files do — your code assumes that there will always be exactly one image, so if you find multiples, it’ll ignore all but the last one).