Why Does My Hit the Road Dude Clip Look Washed Out After Editing? Checking Color Range Tags With ffprobe and Python

本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。

Why Does My Hit the Road Dude Clip Look Washed Out After Editing? Checking Color Range Tags With ffprobe and Python

为什么我的“Hit the Road Dude”剪辑在编辑后看起来发灰?使用 ffprobe 和 Python 检查色彩范围标签

Checked November 4, 2026 (09:00 CST, UTC+8) against the live pages linked below. I build hittheroaddude.app, an independent studio that remakes the Hit the Road Dude (State Property) pickup-window clip from one photo. The script in this post works on any H.264 MP4. It doesn’t depend on my studio.

于 2026 年 11 月 4 日(北京时间 09:00)对照下方链接的实时页面进行了核对。我开发了 hittheroaddude.app,这是一个独立工作室,可以将一张照片重制为“Hit the Road Dude”(State Property)皮卡车窗剪辑。本文中的脚本适用于任何 H.264 MP4 文件,且不依赖于我的工作室。

A question I keep getting, in different words: “My Hit the Road Dude clip looked fine on my phone, then I ran it through an editor and now it’s grey and flat.” Or the reverse: “the black truck interior turned into a black blob and the sky is blown out.” Neither one is usually the AI’s fault. The pixels didn’t change. What changed is how a player was told to read them.

我不断收到一个问题,虽然措辞各异,但核心都是:“我的 Hit the Road Dude 剪辑在手机上看很正常,但经过编辑器处理后,画面变得灰暗平淡。”或者反过来:“黑色卡车内饰变成了一团黑影,天空也过曝了。”通常这并不是 AI 的错。像素本身没有改变,改变的是播放器读取这些像素的方式。

Every H.264 file carries (or fails to carry) a few small colour tags, and when the tags and the pixels disagree, the same file looks different in every app. This post shows how to check that in 15 seconds with ffprobe, a raw read of the luma plane, and a 90-line Python script. It also shows the two fixes, and which one re-encodes.

每个 H.264 文件都携带(或未能携带)一些小的色彩标签。当标签与像素不匹配时,同一个文件在不同的应用程序中看起来就会不同。本文将展示如何通过 ffprobe、读取原始亮度平面以及一个 90 行的 Python 脚本,在 15 秒内完成检查。文中还介绍了两种修复方法,并说明了哪种方法需要重新编码。

The short version: Limited range (“tv”) means luma lives between 16 and 235. That’s what almost all phone and web video uses. Full range (“pc”) means luma uses 0 to 255. Some cameras, screen recorders and image-to-video exports write this. If a limited file is tagged full, black is drawn at 16 instead of 0 and it looks washed out. If a full file is tagged limited, everything under 16 and over 235 clips: crushed blacks, blown whites. If the file is untagged, each app guesses. That’s why it can look right in one place and wrong in the next. You can’t tell which of these you have by looking. You can measure it.

简而言之:受限范围(“tv”)意味着亮度值在 16 到 235 之间,这是几乎所有手机和网络视频使用的标准。全范围(“pc”)意味着亮度使用 0 到 255。一些摄像机、屏幕录制软件和图像转视频的导出工具会使用这种格式。如果一个受限范围的文件被标记为全范围,黑色部分会以 16 而非 0 显示,导致画面发灰。如果一个全范围文件被标记为受限,低于 16 和高于 235 的部分会被截断:导致黑色细节丢失、白色过曝。如果文件没有标签,每个应用程序都会进行猜测,这就是为什么它在某些地方看起来正常,而在另一些地方却显示错误。你无法仅凭肉眼判断属于哪种情况,但可以通过测量得出结论。

Step 1: read the tags ffprobe -v error -select_streams v:0
-show_entries stream=pix_fmt,color_range,color_space,color_primaries,color_transfer
-of default=nw=1 clip.mp4 A healthy phone-ready file prints something like color_range=tv, color_space=bt709, color_primaries=bt709, color_transfer=bt709. unknown on any of these lines means “nobody said, the app will guess.”

第一步:读取标签 ffprobe -v error -select_streams v:0
-show_entries stream=pix_fmt,color_range,color_space,color_primaries,color_transfer
-of default=nw=1 clip.mp4 一个健康的、适用于手机的文件会显示类似 color_range=tv, color_space=bt709, color_primaries=bt709, color_transfer=bt709 的信息。如果其中任何一行显示 unknown,则意味着“没有明确说明,应用程序将自行猜测”。

Step 2: look at the pixels, not just the label. Tags are claims. To check them, read the actual Y (luma) plane and count how many samples fall outside 16..235. Two things matter here: Use the raw plane. -vf extractplanes=y -pix_fmt gray hands you the stored luma bytes as they are. If you convert to RGB first, ffmpeg applies a range conversion and you end up measuring your own conversion. Use a share, not min/max. My first version used signalstats YMIN/YMAX. x264 ringing at sharp edges (text, the window frame) pushes a few samples to 0 or 255 even in a perfectly normal limited-range file, so min/max said “full range” for everything. Counting the share of samples outside 16..235 and using a 1% threshold fixed that.

第二步:观察像素,而不仅仅是标签。标签只是声明。要进行核实,请读取实际的 Y(亮度)平面,并计算有多少样本落在 16 到 235 范围之外。这里有两点很重要:使用原始平面。-vf extractplanes=y -pix_fmt gray 会直接提供存储的亮度字节。如果你先转换为 RGB,ffmpeg 会应用范围转换,导致你测量的是你自己的转换结果。使用比例,而不是最小值/最大值。我的第一个版本使用了 signalstats 的 YMIN/YMAX。x264 在锐利边缘(如文字、窗框)产生的振铃效应会将少量样本推向 0 或 255,即使在完全正常的受限范围文件中也是如此,因此最小值/最大值会判定所有文件为“全范围”。通过计算超出 16..235 范围的样本比例并使用 1% 的阈值,解决了这个问题。

The script: #!/usr/bin/env python3 """color_check.py - why does my vertical clip look washed out or too contrasty? Compares the colour tags an MP4 declares (ffprobe) with the luma levels its pixels actually use (raw Y plane sampled at 2 fps), then prints a fix. usage: python3 color_check.py clip.mp4 exit 0 = tags present and consistent, 1 = something to fix """ … (Python code omitted for brevity) … It needs Python 3, numpy and ffmpeg/ffprobe on your PATH (pip install numpy). Sampling at 2 fps means a 15-second clip is 30 frames, which took under half a second on my box.

脚本: #!/usr/bin/env python3 """color_check.py - 为什么我的竖屏剪辑看起来发灰或对比度过高?该脚本将 MP4 声明的色彩标签(ffprobe)与像素实际使用的亮度级别(以 2 fps 采样的原始 Y 平面)进行比较,然后打印出修复方案。用法:python3 color_check.py clip.mp4,退出代码 0 = 标签存在且一致,1 = 需要修复 """ …(此处省略 Python 代码)… 它需要 Python 3、numpy 以及在 PATH 中的 ffmpeg/ffprobe(通过 pip install numpy 安装)。以 2 fps 采样意味着 15 秒的剪辑包含 30 帧,在我的机器上处理时间不到半秒。

What it printed on four test files: I made four 15-second 720×1280 test clips with testsrc2, a black bar and a white bar, and a synthetic voice track. One is untagged, one is tagged correctly, o…

它在四个测试文件上的输出:我使用 testsrc2 制作了四个 15 秒的 720×1280 测试剪辑,包含黑条、白条和合成语音轨道。其中一个没有标签,一个标签正确,o…