How a folder of 228 high-resolution graphic novel panels became a polished teaser video, and why Go was the right language for the job.
The Project
The Odyssey Illustrated is a graphic novel rendition of Homer's epic — panel by panel, book by book. You can see the result in action on YouTube. At the time of this writing, the project had accumulated 228 hand-crafted PNG panels, each clocking in at 1536×1024 pixels and roughly 2.9 megabytes. The natural next step was obvious: a teaser video. Something short, punchy, and shareable — a 15-second window into the full work that could live on social media, in newsletters, or at the top of a landing page.
The requirements were straightforward. Sample every fourth panel to get a manageable subset. Resize them to 1080p. Stitch them together with crossfade transitions. Add a dramatic audio track. Output an MP4. A weekend project, right?
It turned out to be one — but not in the way I expected. The language I reached for first was Python, and within an hour I was reaching for something else.
Why Python Falls Short Here
Python is excellent for a huge range of tasks. Video processing of large image sets, however, exposes some of its rougher edges.
The first problem is memory. Pillow, Python's go-to image library, loads entire images into RAM as uncompressed pixel arrays. A single 1536×1024 RGB image becomes roughly 4.7 megabytes in memory — nearly double its on-disk size. At 228 images, that's over a gigabyte just for the source frames, before any processing begins. There's no streaming decode, no lazy loading. You either fit everything in memory or you don't.
The second problem is the ecosystem. MoviePy, the most popular Python video editor, is a convenience wrapper around ffmpeg subprocess calls. It works for simple tasks, but the pipeline is fragile. Frames pass through pipes between Python and ffmpeg, with format conversions at every boundary. Debugging failures means tracing errors across two runtimes. And the API, while friendly, hides enough of the underlying mechanics that performance surprises are common.
Then there's the GIL. Python's global interpreter lock means that CPU-bound work — like resizing hundreds of images — runs on a single core regardless of how many you have. You can multiprocess around it, but that introduces its own complexity: shared memory, serialization overhead, process management.
Finally, there's the typing problem. When you're computing image dimensions across a pipeline of resize, pad, and transition operations, dimension mismatches are the kind of bug that should be caught at compile time. In Python, they surface at runtime, often after you've already processed fifty images and attempted a crossfade that silently fails.
None of these are dealbreakers in isolation. Together, they added up to friction I didn't want for a tool that should have been simple.
The Go Advantage
Go handles this kind of work with a quiet competence that's hard to overstate.
The standard library includes image/png and image/jpeg — full decode and encode with zero external dependencies. For resizing, golang.org/x/image/draw provides quality interpolation modes including bilinear and Catmull-Rom. No pip install, no version conflicts, no ABI mismatches. Just import and use.
The type system caught bugs early. When I defined the resize function to return image.RGBA, the compiler enforced that every downstream consumer worked with the correct dimensions. The padding step — necessary because moviego requires all frames to share identical pixel dimensions for transitions — was trivial to verify at compile time.
Deployment is a single binary. No virtualenvs, no dependency trees, no "works on my machine." The tool compiles to a statically linked executable that runs anywhere Go is supported.
And the concurrency model, while not fully exploited in this project, is built in. Goroutines and channels are there when you need them. A future version could resize frames in parallel across all available cores with minimal code changes.
Enter MovieGo
The piece that made this project viable in Go was MovieGo — an open-source video editing library that wraps ffmpeg with a typed, lazy clip graph.
Where MoviePy feels like a script runner, MovieGo feels like a compiler for video operations. You describe what you want — open these images, resize them, chain them with crossfades, attach an audio track — and the library figures out the most efficient way to produce it. When the entire graph is expressible as an ffmpeg filtergraph, it can skip Go pixels entirely and run everything in a single ffmpeg invocation.
The API is worth noting. MovieGo uses a fluent builder pattern for transitions: you create a sequence, add clips, and insert crossfades between them with method calls. Frame rates are expressed as rational numbers — Rate{Num, Den} — never floating-point, so there's no drift over long sequences. The library is MIT licensed, well-documented, and actively maintained.
For the Odyssey teaser, the core pipeline is about 270 lines of Go. It discovers PNG files in a directory, samples them at a configurable step, resizes each to a target height while maintaining aspect ratio, pads any narrower frames to uniform dimensions, builds a sequence with optional crossfades, optionally attaches and shapes an audio track, and encodes the final H.264 MP4 with AAC audio.
The audio handling deserves a mention. MovieGo makes it trivial to open an audio file, loop it if it's shorter than the video, truncate it if it's longer, apply volume scaling and fade-in/fade-out envelopes, and mux it into the output. What would be a multi-hour debugging session in Python is five lines of method calls in Go.
Iterating on Quality
The first version of the teaser used crossfade transitions between every panel — a 100ms dissolve that blended adjacent frames into each other. It looked slick in theory, but in practice the crossfades introduced a noticeable blur. Every frame spent part of its display time at partial opacity blended with its neighbor, creating a ghosting effect that softened the crisp linework of the graphic novel panels.
The fix was a flag: -crossfade=true (default) or -crossfade=false for hard cuts. When disabled, each image displays at full opacity for its exact duration, then snaps to the next. No blending, no ghosting. For a project where every panel is a hand-illustrated work of art, hard turns out to be the better choice.
The resize quality got an upgrade too. The initial version used bilinear interpolation — fast but slightly soft. Switching to Catmull-Rom scaling produced noticeably sharper results at the cost of a marginal increase in resize time, irrelevant for a batch of 57 frames. The difference is subtle but real: edges are cleaner, fine details hold up better at 1080p, and the panels look like they belong in a video rather than a resize.
These are small changes, but they reflect the kind of iterative refinement that a good tool should support. Add a flag, swap an interpolation kernel, rebuild in two seconds, compare the output. Go's compilation speed makes this workflow frictionless.
The Result
The final teaser is a 21 megabyte MP4 — 1620×1080, H.264 video at 3.8 fps, AAC stereo audio, 57 panels with hard cuts and a dramatic soundtrack. It encodes in seconds on a modern machine. The source is a single Go file with two dependencies: MovieGo and the extended image library.
More importantly, the tool is reusable. Change the -src directory, adjust the -step and -duration flags, swap in a different audio track, toggle crossfades on or off, and you have a teaser for any sequential image set. It's a small program, but it does exactly what it claims to, reliably, in a language that doesn't get in your way.
If you're building video pipelines and you haven't looked at Go, you should. And if you do, take a hard look at MovieGo. It's the kind of open-source project that quietly changes what's possible.
The Odyssey Illustrated is a graphic novel rendition of Homer's epic. Watch the trailer on YouTube. The teaser tool is available as a standalone project at this GitHub repo.

Top comments (0)