从YUV文件中提取出特定的某一帧有哪几种方式?
这种场合一般发生在需要帧同步调试/特别对比某两帧/查看内容/批处理的场合,不过其实专门讨论意义有限。因为往往都有图形化界面自由查看。
因此这其实是一个相对无聊的问题。当然,换一种方式也可以:对于像YUV这样一个规整的格式,有多少种工具可以拆分文件?
p.s. 以下都以YUV420格式 为例子。其每帧大小为 <math xmlns="http://www.w3.org/1998/Math/MathML"> W × H × 3 2 W\times H \times \frac{3}{2} </math>W×H×23.
FFmpeg
- 如果手头有FFmpeg工具,这是坠吼滴。
css
ffmpeg -s [width]x[height] -i input.yuv -vframes 1 -f rawvideo -pix_fmt yuv420p output.yuv
-vframes 1
表示只处理一个视频帧。
- 视场景设置,改换为帧率指定也是可以的。
例如,对于30fps的视频,提取第10秒的一帧。即第300帧:
css
ffmpeg -s [width]x[height] -r 30 -i input.yuv -ss 00:00:10 -vframes 1 -f rawvideo -pix_fmt yuv420p output.yuv
Python
高级语言就任意折腾就好。一般都是系统之一的模块。
python
width = 1920 # 示例宽度
height = 1080 # 示例高度
frame_number = 1 # 提取第1帧
# YUV420格式一帧数据大小
frame_size = width * height + (width // 2) * (height // 2) * 2
with open('input.yuv', 'rb') as file:
file.seek(frame_size * (frame_number - 1))
frame_data = file.read(frame_size)
with open(f'output_{width}x{height}.yuv', 'wb') as output_file:
output_file.write(frame_data)
C
同上,高级语言任意折腾。常规 File IO.
ini
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *file = fopen("input.yuv", "rb");
if (file == NULL)
{
perror("Error opening file");
return 1;
}
int width = 1920; // 宽度
int height = 1080; // 高度
int frame_number = 1; // 提取的帧号
// YUV420格式一帧数据大小
int frame_size = width * height + (width / 2) * (height / 2) * 2;
unsigned char *frame_data = (unsigned char *)malloc(frame_size);
if (frame_data == NULL)
{
perror("Memory allocation failed");
fclose(file);
return 1;
}
fseek(file, (long)frame_size * (frame_number - 1), SEEK_SET);
fread(frame_data, 1, frame_size, file);
FILE *output_file = fopen("output.yuv", "wb");
if (output_file != NULL)
{
fwrite(frame_data, 1, frame_size, output_file);
fclose(output_file);
}
else
{
perror("Error opening output file");
}
free(frame_data);
fclose(file);
return 0;
}
Command line
其实主要是想折腾这种方式,即在没有FFmpeg和高级语言支持下怎么(顺手)完成规整文件读写这样的小事。
dd
可以考虑dd
,直接指定文件读写的位置和大小。
例如,1920x1080 YUV 420 提取第 <math xmlns="http://www.w3.org/1998/Math/MathML"> i i </math>i帧:
bash
dd if=input.yuv of=output_frame.yuv bs=1 count=$((1920*1080*3/2)) skip=$(( [i-1] *1920*1080*3/2))
其中, -if=input.yuv
指定输入文件; -of=output_frame.yuv
指定输出文件; -bs=1
设置块大小为1字节; -count=$((1920*1080*3/2))
指定复制的字节数,即一帧的大小; -skip=$((4*1920*1080*3/2))
跳过前 <math xmlns="http://www.w3.org/1998/Math/MathML"> i − 1 i-1 </math>i−1帧的数据量,以提取第 <math xmlns="http://www.w3.org/1998/Math/MathML"> i i </math>i帧.
split & cat
或者直接使用split
分帧 (split
分帧是确实方便,就是文件命名比较折腾)
bash
# 分帧,以frame_xx命名,xx遵循字母计数法。
split -b $((1920*1080*3/2)) input.yuv frame_
# 例如,将某一帧(_aa,26x25 帧范围内转换出来的第一帧)转换为输出文件
cat frame_aa > output.yuv
当然,因为split
命令生成的文件后缀是字母计数法 。是按照字母顺序递增的,从 aa
开始,然后是 ab
,以此类推。所以会遗留一堆不需要的分帧。
解决这个问题的话可以通过一个粗糙的shell脚本,其编写了convert_number_to_suffix
函数,实现了最简单粗暴的字母计数法转换(ASCII码直转),可以处理帧数为26~676的视频。
bash
#!/bin/bash
# 字母计数法和阿拉伯数字转换脚本,范围为 26~676(即26x26)
convert_number_to_suffix() {
local num=$1
local -i charcode_a=97
local -i first_digit=$(( (num - 1) / 26 + charcode_a ))
local -i second_digit=$(( (num - 1) % 26 + charcode_a ))
local first_char=$(printf \$(printf '%03o' $first_digit) )
local second_char=$(printf \$(printf '%03o' $second_digit) )
echo "$first_char$second_char"
}
if [ "$#" -ne 4 ]; then
echo "Usage: $0 <input.yuv> <width> <height> <frame_number>"
exit 1
fi
# 输入的YUV文件路径
INPUT_FILE=$1
# 视频的宽度
WIDTH=$2
# 视频的高度
HEIGHT=$3
# 提取的帧序号
FRAME_NUMBER=$4
FRAME_SIZE=$((WIDTH * HEIGHT * 3 / 2))
FILE_SUFFIX=$(convert_number_to_suffix $FRAME_NUMBER)
split -b $FRAME_SIZE $INPUT_FILE frame_
cat "frame_$FILE_SUFFIX" > "output_frame_$FRAME_NUMBER.yuv"
# 清理其他分割文件
rm frame_*
与其说是在折腾分帧,不如说是在折腾命名方法
总结
已经折腾得简单问题复杂化了。
TODO:
- 对比高级语言脚本实现与dd/cat方式的性能分析
- 字母序列计数法的实现讨论,我们能实现一个更完善的版本(当然,那会更复杂,而且属于另一个问题了)