How to Batch-Extract Film Noir Still Frames Using FFmpeg and a C++ Wrapper
Last Tuesday, I had put on the Touch of Evil Blu-ray I'd borrowed from the Ottawa library and was trying to capture specific scenes — I was on roughly my thirty-seventh manual screenshot. Right hand on the spacebar, left hand on a shortcut key. Right around the time I realized no human should be doing this, I remembered the FFmpeg command line I'd briefly used for a class assignment a while back. Time to automate. I had a C++ assignment piling up anyway, so I decided to kill two birds with one stone.
Why FFmpeg
You can pull stills with a GUI editing tool, but when you need to analyze dozens of noir films, opening each one and hitting Export is a chore. FFmpeg lets you extract just the keyframes with a single CLI command. The basic command looks like this:
ffmpeg -i input.mkv -vf "select=eq(pict_type\,I)" -vsync vfr out_%04d.png
select=eq(pict_type\,I) — this filters for only I-frames, which correspond to scene changes or the start of a sequence. In a genre like noir, where lighting shifts are dramatic, these I-frames make a great starting point for shot-composition analysis.

C++ Wrapper Structure
Copy-pasting file paths into the terminal every time hardly qualifies as automation. So I put together a simple C++ wrapper. Here's the gist of the structure:
#include <filesystem>
#include <cstdlib>
#include <string>
namespace fs = std::filesystem;
void extractFrames(const fs::path& inputDir, const fs::path& outputDir) {
for (auto& entry : fs::directory_iterator(inputDir)) {
if (entry.path().extension() == ".mkv" ||
entry.path().extension() == ".mp4") {
std::string outPrefix = (outputDir / entry.path().stem()).string();
std::string cmd = "ffmpeg -i \"" + entry.path().string()
+ "\" -vf \"select=eq(pict_type\\,I)\" -vsync vfr \""
+ outPrefix + "_%04d.png\"";
std::system(cmd.c_str());
}
}
}
Using std::filesystem keeps directory traversal clean. Dump your .mkv or .mp4 files into the input folder and run it — it automatically separates output by film name and extracts the stills. You'll need C++17 or later to compile:
g++ -std=c++17 -o noir_extract main.cpp
One thing to watch out for: calling external commands through std::system is fine for educational or personal projects, but it can break if filenames contain shell special characters. In my case, I just cleaned up filenames beforehand. I'm in the habit of organizing my movie files anyway.
Brightness-Based Filtering — Because It's Noir
Extracting every I-frame gives you a lot. I once got over 700 frames from a single run of The Third Man. What I'm most interested in when analyzing noir shot composition is the contrast in low-light scenes. So I added one more condition to the FFmpeg filter — a brightness threshold:
-vf "select='eq(pict_type,I)*lt(avg(luma),80)'"
This keeps only frames where the avg(luma) value is below 80. The exact number needs adjusting per film — I usually start at 60 and raise it after reviewing the results. By filtering out bright scenes, the frames I actually want to analyze — dark hallways, silhouettes under streetlights — show up at a much higher rate.

What I Learned From Actually Using It
After building this tool, I ran it on about fifteen films, from Double Indemnity to Detour. What I found more satisfying than the speed was the consistency. Lining up frames extracted with the same criteria, I started noticing things like each director's lighting habits. When I was taking screenshots manually, my own preferences crept in and blurred the comparisons.
If you run into any issues with the build or installing FFmpeg, leave a comment. I'm thinking about wrapping this in a GUI before Ottawa's winter sets in, but that depends on how many more C++ template error messages my patience can endure.
Comments