Time-Lapse: Raspi Zero, Part 2
A year ago, I set up a Raspberry Pi Zero to lay the groundwork for a time-lapse video capturing a tree as the seasons changed over the course of a year. At 18 shots per day, over 7,000 images were captured (it actually took over 13 months), and the story must continue.
The Raspberry Pi Zero 2 W has built-in Wi-Fi, which is quite handy—given the lack of an Ethernet port—for downloading the photos every now and then and keeping its operating system up to date. I don’t really trust an SD card that’s in constant use. I briefly considered setting up a share on my server (Synology NAS) directly, but I trust Wi-Fi even less. So I regularly downloaded the photos via SCP and then deleted them from the SD card after updating the operating system. At some point during the year, due to an unresolved issue, I changed the Wi-Fi SSID and set the original SSID as a guest network. Unfortunately, the Raspberry Pi wouldn’t switch to the new Wi-Fi network, so to access it, I always had to connect my laptop to the guest network—no big deal, or so I thought. After a few days, the Raspberry Pi stopped responding to requests to establish an SSH connection. I waited until the last photo of the day (4:30 p.m.) was supposed to be taken and then briefly unplugged it—without moving it, so as not to change the framing. It turned out that the little rascal had been working away diligently and had taken all the photos as desired. After a few weeks, however, even after I briefly unplugged it, it still wouldn’t accept an SSH connection. So I just left it alone without backing up the photos or updating the operating system.
Once the planned shooting period was over, all I had to do was find a way to access the photos. Windows can’t handle the ext4 file system. Even trying to use the Windows Subsystem for Linux (WSL2) didn’t work right away, and before I invested a lot of time setting up an iSCSI broker that I’d probably never need again, I used an existing USB drive with a Linux Mint live system to copy the images to the server.
Since I can't get my preferred video editing program to run on my laptop and I currently don't have the energy to set up a long session on my desktop, I was hoping to find a simple yet powerful tool in FFmpeg—but I was way off the mark—at first. I had the individual photos saved with the date and time in the filename (img-20250312-08_00.jpg) so they’d be in the correct order right away—which was certainly a good idea for a video editor with a GUI—but ffmpeg on Windows doesn’t support the parameter -pattern_type glob ‘*.jpg’, which would have allowed it to process the files in the correct order on its own. As I learned, this requires POSIX compatibility (which Windows lacks), so I had to find another solution. With the help of Claude, I decided to take a small detour and created a text file for a chained command that ffmpeg could process.
First, however, I had to review and clean up the files. I copied the images from the server to a SATA SSD on my laptop to E:\Zeitraffer\, since I wanted to keep the network—with its latency—out of the encoding process. Then I selected a one-year period (March 12, 2025–March 11, 2026) and deleted all images outside that range. In File Explorer’s thumbnail view, I then deleted all images that were too dark or completely black, since the recording period of 8:00 a.m.–4:30 p.m. was during the fall - I’d been too generous with the winter months. But actually, the original idea was to select 3–4 images per day and not process them all—yet as I reviewed them, I really liked the sun circling the sky and the melting snow.
Processing
You can create the file list using Windows' built-in tools in PowerShell:
Get-ChildItem E:\Zeitraffer\*.jpg | \
Sort-Object Name | ForEach-Object { "file '$($_.fullname)'" } | \
Out-File -Encoding utf8 E:\liste.txt
- Get-ChildItem returns a list of all files in the directory
E:\Zeitraffer\that match the pattern*.jpg. - Sort-Object sorts the list by its Name attribute.
- ForEach-Object then outputs a string for each file that contains the value of the Fullname attribute. Example:
file ‘E:\Zeitraffer\img-20250312-08_00.jpg’ - Out-File then writes this continuously, line by line in UTF-8 format, to the file
E:\liste.txt
ffmpeg can be installed from a PowerShell window run with administrative privileges:
winget install ffmpeg
Now you can pass the task to ffmpeg in PowerShell:
fmpeg -r 30 -f concat -safe 0 \
-i e:\liste.txt -c:v libx265 -crf 20 \
-pix_fmt yuv420p C:\Users\cs\Documents\zeitraffer.mp4
- The parameter
-r 30sets the video frame rate to 30 frames per second. - The parameter
-f concatensures that multiple media files are seamlessly merged into a single file, while-safe 0allows the use of files in the current directory. - The parameter
-i e:\liste.txtpasses the previously created list of image files. - The parameter
-c:v libx265selects the H265 codec for encoding the video. - The parameter
-crf 20allows x265 to automatically adjust the bitrate, resulting in better quality than the default value of 23 with a fixed bitrate. - The parameter
-pix_fmt yuv420pforces the video to be output in the YUV 4:2:0 pixel format, which is required for maximum compatibility with browsers and platforms. - The last parameter then specifies the name and path of the output file, in this case on an NVMe SSD.
Example
In the end, there were 6,514 files left with a resolution of 2,592x1,944 pixels, resulting in a 1.85 GB video. To illustrate this, I re-encoded it using HandBrake at 800x600 with higher compression. This reduced the file size to just 192 MB.
In the Fediverse, you can join the discussion and ask questions.