Project: r/place timelapses - Chapter3
Read First:
Creating the cropped timelapses!⌗
Anyone can create a full timelapse from images with ffmpeg, it’s just a oneliner like this (framerate is based on 8622 files / 60 seconds):
ffmpeg -framerate 144 -pattern_type glob -i '*.png' -c:v libx264 -pix_fmt yuv420p -filter:v fps=fps=60 FULL.mp4
Instead my niche was to create cropped, zoomed in timelapses of different artworks. The canvas is huge and different communities / subreddits claimed their piece and worked on it throughout the event.
But to easily communicate requests and pick the right areas to crop - I needed some tooling. The Atlas was already established - it was the perfect place to pick coordinates.
Calculating coordinates on the other hand.. At first I started doing it manually by calculating a crop over the area, scalable with 1920x1080.
That got tedious quickly and I created a script which over time grew to this:
Calculate the crop area and aspect ratio⌗
import os
import climage
# Function to sanitize the file/directory-naming
def cleanFilename(sourcestring, removestring=" %:/,.\\[]<>*?"):
return ''.join([c for c in sourcestring if c not in removestring])
# Function to calculate 16:9 or 9:16 aspect ratio based on input
def image_to_ratio(w, h):
User_Width= w
User_Height= h
while True:
base = w//16 if w/h < 16/9 else h//9
base_resolution = ((base * 16), (base * 9))
# if the suggested resolution is smaller than the user defined, pad with 4 pixels and retry
if base_resolution[0] < User_Width or base_resolution[1] < User_Height:
w+=4
h+=4
else:
if User_Width > User_Height: # portrait or landscape
return base * 16, base * 9, (1080 / (base * 9)) * 100
else:
return base * 9, base * 16, int((1080 / (base * 9)) * 100)
# User input:
name = cleanFilename(input("Name of Community/Artwork: "))
start = list(int(x.strip()) for x in input("Start coordinates: (Top left corner - example: -327,-186) ").split(','))
stop = list(int(x.strip()) for x in input("Stop coordinates: (Bottom right corner - example: 94,-74) ").split(','))
# Adjust for negative coordinates by the atlas: w-1500,h-1000
# The canvas is 3000x2000 and their coordinates is 0 in the center
# While the image processing is 0 in top left corner
start[0] += 1500
start[1] += 1000
stop[0] += 1500
stop[1] += 1000
# calculate resolution based on coordinates
user_resolution = ((stop[0]-start[0]), (stop[1]-start[1]))
print(f"UserRes: {user_resolution}")
# calculate 16:9 aspect ratio and scale percentage
ratio = image_to_ratio(user_resolution[0], user_resolution[1])
print(f"ClosestRes + Multiplier: {ratio}")
# new starting coordinates based on resize of cropped area
new_start = (start[0]-int((ratio[0]-user_resolution[0])/2), start[1]-int((ratio[1]-user_resolution[1])/2))
# create preview of the final image, with my template-canvas.png as base
os.system(f"\nconvert canvas.png -crop {ratio[0]}x{ratio[1]}+{new_start[0]}+{new_start[1]} -filter point -resize {ratio[2]}% test_{name}.png")
# print the line to use in the next step - timelapsing!
print(f"\nconvert canvas.png -crop {ratio[0]}x{ratio[1]}+{new_start[0]}+{new_start[1]} -filter point -resize {ratio[2]}%")
# climage - preview the image in the terminal:
preview = climage.convert(f"test_{name}.png", is_unicode=True, is_truecolor=True, is_256color=False, is_16color=False, is_8color=False, width=120, palette="default")
print(preview)
With that done, I got a preview out of the final crop and the variables I need for the next script, the actual timelapse.
Time for timelapse!⌗
Now we have the values from the previous script to add to the Crop
-variable.
Then I wanted to create a way of choosing start time, as many artworks were in areas not “unlocked” until later in the event. We’ve got the filenames as epoch-time still and people grab the coordinates from the Atlas - so the tooltip timestamp is what we’ll use.
#!/bin/bash
Crop='-crop 48x27+49+1595 -filter point -resize 4000.0%'
ProjectName="DFTBA"
Timestamp="Mon, 24 Jul 2023 02:30:00 GMT"
Length=30 # length of the output clip in seconds
Epoch=$(date -d "${Timestamp} -30 minutes" +"%s")
IFS=$'\n'
FileArray=( "$(ls -1 -- *.png)" ) # adding all png's to array
FileArray+=("${Epoch}.png") # adding fake timestamp-file to array
FileArray=("$(sort <<<"${FileArray[*]}")") # sorting
unset IFS
# remove everything before timestamp file, including the timestamp-file
CleanArray=$(echo "${FileArray[@]}" | sed -ne '/'"$Epoch"'/,${/'"$Epoch"'/!p;}')
# count frames to calculate length
ImageCount=$(printf '%s\n' "${CleanArray[@]}" | wc -l)
Frames=$(( ImageCount / Length ))
mkdir -p "$ProjectName"
# iterate all the images in the array, crop+scale
Count=0
for f in $CleanArray ; do
(( Count++ ))
convert "$f" $Crop "$ProjectName"/"${f/.png/-cropscale.png}"
# made a counter to show the process
printf "Processed %s of %s\033[0K\r" "${Count}" "${ImageCount}"
done
cd "$ProjectName" || exit # quit if there's no directory
# stitch together the clip from the cropped images
ffmpeg -framerate $Frames -pattern_type glob -i '*.png' -c:v libx264 -pix_fmt yuv420p -filter:v fps=fps=60 $ProjectName.mp4
cd .. # go back to previous directory before quitting
exit
So that’s that! A timelapse is created.
- Pick coordinates from the Atlas
- Run through a python script to calculate the crop ratio and resolution
- Run through a bash script to mass-crop the images and finally create the timelapse
Cant we do all that in one go?⌗
Well yeah, and over time I kept adding functionality to it so now its rather big considering where we started..
Terminal screenshot of lapsing the Ukraine-flag, coordinates picked from the Atlas:
Still got some cleaning to do.. but at the time of writing - this is it:
import sys
import glob
import os
from datetime import datetime
import calendar
import climage
raw_images = "ALL/" # location of raw images
canvas_temp = "canvas.png" # template canvas file
clip_length=30 # length of the output clip in seconds
def clean_dirname(sourcestring, removestring=" %:/,.\\[]<>*?"):
return ''.join([c for c in sourcestring if c not in removestring])
def image_to_ratio(w, h):
user_width= w
user_height= h
while True:
base = w//16 if w/h < 16/9 else h//9
base_resolution = ((base * 16), (base * 9))
# if the suggested resolution is smaller than the user defined, pad with 4 pixels and retry
if base_resolution[0] < user_width or base_resolution[1] < user_height:
w+=4
h+=4
else:
if user_width > user_height: # portrait or landscape
return base * 16, base * 9, (1080 / (base * 9)) * 100
else:
return base * 9, base * 16, int((1080 / (base * 9)) * 100)
def cd(dir):
if os.path.exists(dir):
os.chdir(dir)
else:
print(f"{dir} does not exist, exiting.")
exit()
def uniq_dir(dir):
counter = 1
dir_name = dir
while os.path.exists(dir):
dir = f"{dir_name}_{counter}"
counter += 1
return dir
def progress_counter(current, total):
percentage = round(100.0 * current/float(total),1)
sys.stdout.write(f"{current}/{total} images processed, {percentage}% complete \r")
sys.stdout.flush()
# User input:
project_name = clean_dirname(input("Name of Community/Artwork: "))
start = list(int(x.strip()) for x in input("Start coordinates (format -123,456) :").split(','))
stop = list(int(x.strip()) for x in input("Stop coordinates (format -456,789) :").split(','))
timestamp = input("Atlas Timestamp for starting (leave blank for default): ") or "Thu, 20 Jul 2023 13:01:20 GMT"
# Adjust for negative coordinates by 2023.place-atlas.stefanocoding.me , w-1500,h-1000
start[0] += 1500
start[1] += 1000
stop[0] += 1500
stop[1] += 1000
# calculate resolution based o coordinates
user_resolution = ((stop[0]-start[0]), (stop[1]-start[1]))
# calculate 16:9 aspect ratio and scale percentage
ratio = image_to_ratio(user_resolution[0], user_resolution[1])
# new starting coordinates based on resize of cropped area
# Need to add logic for end of the canvas, in all directions
new_start = [start[0]-int((ratio[0]-user_resolution[0])/2), start[1]-int((ratio[1]-user_resolution[1])/2)]
# logic to stay within the canvas max resolution
for i, s in enumerate(new_start):
new_start[i] = max(0, new_start[i])
if (new_start[0]+ratio[0]) > 3000:
new_start[0] = new_start[0] - ((new_start[0]+ratio[0]) - 3000)
if (new_start[1]+ratio[1]) > 2000:
new_start[1] = new_start[1] - ((new_start[1]+ratio[1]) - 2000)
# create preview-image with imagemagick
os.system(f"\nconvert {canvas_temp} -crop {ratio[0]}x{ratio[1]}+{new_start[0]}+{new_start[1]} -filter point -resize {ratio[2]}% test_{project_name}.png")
print("Preview of the cropped area, will look more pixelated due to the terminal:")
## Climage - show image:
preview = climage.convert(f"test_{project_name}.png", is_unicode=True, is_truecolor=True, is_256color=False, is_16color=False, is_8color=False, width=120, palette="default")
print(preview)
# Continue? Long time!
user_choice = input("Looks good? Continue with the convertion (can take a while)? y/n: ")
if not (user_choice.lower() == "yes" or user_choice.lower() == "y") :
exit()
crop_arguments = f"-crop {ratio[0]}x{ratio[1]}+{new_start[0]}+{new_start[1]} -filter point -resize {ratio[2]}%"
epoch = calendar.timegm(datetime.strptime(timestamp, "%a, %d %b %Y %H:%M:%S %Z").utctimetuple())
origin_dir = os.getcwd() # Get current dir for later
raw_images = raw_images.removesuffix("/") # sanitize input
cd(raw_images) # cd to the raw images
file_list = glob.glob("*.png") # list all pngs
file_list.append(f"{epoch}.png") # append fake timestamp-file
file_list.sort() # sort the list
# Grab only images after timestamp
fake_index = file_list.index(f"{epoch}.png") + 1
clean_list = file_list[fake_index:]
# count images to calculate length
image_count = len(clean_list)
frames = image_count / clip_length
# define name and create dir
work_dir = uniq_dir(project_name)
os.mkdir(work_dir)
print(f"Output directory: {work_dir}")
# iterate all the images in the list, crop+scale
que_number = 0
for file in clean_list: # TODO - add a break!
que_number += 1
new_filename = file.replace(".png", "-cropscale.png")
convert_run = f"convert {file} {crop_arguments} {work_dir}/{new_filename}"
os.system(convert_run)
progress_counter(que_number, image_count)
cd(work_dir)
# stitch together the clip from the cropped images
ffmpeg_run = f"ffmpeg -loglevel error -stats -framerate {frames} -pattern_type glob -i '*.png' -c:v libx264 -pix_fmt yuv420p -filter:v fps=fps=60 {project_name}.mp4"
os.system(ffmpeg_run)
cd(origin_dir) # return to the starting directory
print(f"All done! Timelapse at {raw_images}/{work_dir}/{project_name}.mp4")
exit()
Watch the complete timelapse created from the previous screenshot.⌗
Now I’m working on a graphical “pick by mouse” feature and also to auto-pick timestamp based on coordinates.
To be continued..? Currently a work in progress at github: mag37/rplace_timelapse