普通视图

发现新文章,点击刷新页面。
昨天以前首页

使用ffmpeg处理YCbCr422(YUV422-10bit)格式的视频信号

作者 大致
2024年8月4日 15:57

最近搞的项目跟视频采集卡有关。客户的要求是采集的时候视频卡信号要设成YCbCr42210bit格式,从板卡上采集到的图像要将这个原始格式不压缩直接保存成mov文件。另外要能读取自己保存的mov文件,在板卡上播放。
然后我的猪队友同事们给我甩来两个课题:
一是没用明白windows的SinkWriter,非说人家巨硬不支持这种yuv42210bit的保存。
二是视频的连续播放,只能把mov文件拆成一帧一帧,把原始数据喂给板卡。但是之前抄的例子是8bityuv,10bit的不会弄,而先转成bmp再转yuv422的话误差太大。

这一看就不是我能手搓出来的事儿,跟客户申请之后,客户表示可以用ffmpeg的命令行(因为GPL,不能用库的方式使用)。
于是我要处理的两个问题如下:
(一)将YUV422-10bit的原始数据(raw)保存成mov
(二)从自己保存的mov中抽取每一帧的原始数据。

一番搜索之下终于给对付上了。这个格式就挺恶心,ffmpeg的官网更恶心。官网上洋洋洒洒50章,每个参数都说到了,例子却寥寥无几,专有名词一大堆,给人一种生人勿进的的错觉。

将yuv422-10bit的原始数据转成非压缩mov

  • 命令行:
  • ffmpeg -f image2pipe \
           -frame_size 5529600 \
           -vcodec v210 \
           -pix_fmt yuv422p10le \
           -s 1920x1080 \
           -r 60000/1001 \
           -i fromcard.raw \
           -c:v v210 \
           -an -sn \
           result.mov
    

    逐个解释一下各参数的设置

  • -f image2pipe
  • 输入文件的format。这里的format是ffmpeg的demuxer的格式,可以通过ffmpeg -demuxers进行查看。输入原始数据的时候本来应该用rawvideo。但是ffmpeg在处理yuv42210p的原始数据的时候有一个非常奇怪的bug:在计算每一帧数据空间的时候,ffmpeg认为一帧数据的大小是宽*高*4。但是yuv42210p系列的格式,每16个byte里包括6个像素点的信息而不是4个,这就造成了用rawvideo生成的图像会丢失1/4的帧的奇怪现象。ffmpge的官网上曾经记录过这个issue,貌似他们没改好啊。我的这个解决办法实际是在issue的回复里找到的。image2pipe是一种通用的流格式,这里利用了它必须指定帧宽的特性。

  • -frame_size 5529600
  • 与image2pipe配合使用,指定每一帧的大小。5529600=1920x1080x16/6,其余分辨率自己算。注意如果raw不能被这个size整除,会报成信道不足的奇怪错误。

  • -vcodec v210
  • 输入使用的解码器。可以大致理解为YCbCr42210bit≈yuv422p10≈v210。v210就是这种格式的FCC码。

  • -pix_fmt yuv422p10le
  • 输入文件的图形格式,yuv422p10le使用16个字节(128bit)描述相邻的6个像素点,并且每32bit按照小字节序反转。反正就是搞清板卡设置的格式,按是否是小端字节序来觉得是否加最后面的“le”

  • -s 1920×1080
  • 输入图像的分辨率

  • -r 60000/1001
  • 相当于帧率(fps)。如果生成的帧率是整数,可以直接写。但如果帧率是小数,比如59.94,就要写成timescale/duration的形式。

  • -i fromcard.raw
  • 输入文件。我这里是把所有的帧存到了一起,放在同一文件里。

  • -c:v v210
  • 输出文件的视频编码器。因为这个参是要设输出文件,所以一定要放在-i的后面。

  • -an
  • 无音频流。

  • -sn
  • 无字幕流。

  • result.mov
  • 输出文件。

    如果需要存成隔行信号的文件,要在-i后面加一组参

    -vf tinterlace=interleave_top,fieldorder=tff
    

    顶场底场/奇数先偶数先其实有4种排列组合。但是自己编自己解,FFMPEG都能搞定,就不具体说明区别了。

    从mov中抽取每一帧的原始数据

  • 命令行:
  • ffmpeg -i input.mov -c:v v210 frame_%04d.raw
    

    同样逐个参数解释:

  • -i input.mov
  • 输入的mov文件。

  • -c:v v210
  • 输出文件的图像编码格式。与之前相同。其实这个参数是无视输入格式本身的。如果是本篇中的例子,输入与输出的图像相同,那就没有产生转换;如果不同,则ffmpeg会自动将原始格式转为这个参指定的图像编码格式,非常智能。

  • frame_%04d.raw
  • 输出文件。ffmpeg是根据输出文件的扩展名来自动识别输出格式的。想要原始数据,就必须指定成.raw为扩展名。如果不加%d,ffmpeg就会把所有帧存成一大坨文件保存;加了%d,就是逐帧分开保存。

    如果上面生成了隔行文件,那么这里也要增加一组参数

    -filter:v yadif=1

    否则解出来的帧数就会少一半。

    搞定!


    • (1):不像《侍魂》和《天外魔境》那样还需要把武器拾回来,而是每个人的武器都能飞去来。

    【翻译】让cheat engine(CE)支持大字节序搜索

    作者 大致
    2023年11月20日 21:51

    Cheat Engine是当下我所知的唯一好用的Windows下的内存修改器。这个工具有个巨大的缺陷——搜索的数据只能以小端(small endian)排列。搜索双字节或四字节数据时,并不像国人开发的上个时代的FPE或者金山游侠那样,自动转换字节序,只能先用计算器把数据拆成字节,然后用字符串数组才能搜索到结果,非常不便。
    今天偶然被我找到了支持多字节大端字节序的方法,特此分享。

    来自作者的github

    照例,我没用过汉化版的CE,故直接用英文菜单描述,如有不便请忍耐。

    1.打开Cheat Engine
    2.打开任意进程
    3.在【Value Type】下拉列表框上单击鼠标右键
    4.点击【Define new custom type (Auto Assembler)】
    20231120_cheat_engine_1
    5.用下面的代码覆盖弹出对话框里的代码
    6.点击OK
    7.重复步骤3-6,把三份代码都添加进去。分别是float的大字节序(Float Big Endian),四字节的大字节序(4 Bytes Big Endian)和双字节的大字节序(2 Bytes Big Endian)。
    8.重启CE(否则新添加的工具不好用)

    代码1:Float Big Endian

    alloc(TypeName,256)
    alloc(ByteSize,4)
    alloc(ConvertRoutine,1024)
    alloc(ConvertBackRoutine,1024)
    alloc(UsesFloat,1)
     
     
    TypeName:
    db 'Float Big Endian',0
     
     
    ByteSize:
    dd 4
    
    UsesFloat:
    db 1
     
     
    ConvertRoutine:
    [64-bit]
    xor eax,eax
    mov eax,[rcx] //eax now contains the bytes 'input' pointed to
    bswap eax //convert to big endian
    ret
    [/64-bit]
    
    [32-bit]
    push ebp
    mov ebp,esp
    mov eax,[ebp+8] //place the address that contains the bytes into eax
    mov eax,[eax] //place the bytes into eax so it's handled as a normal 4 byte value
    bswap eax
    pop ebp
    ret 4
    [/32-bit]
     
     
    ConvertBackRoutine:
    [64-bit]
    bswap ecx //convert the little endian input into a big endian input
    mov [rdx],ecx //place the integer the 4 bytes pointed to by rdx
    ret
    [/64-bit]
    
    [32-bit]
    push ebp
    mov ebp,esp
    push eax
    push ebx
    mov eax,[ebp+8] //load the value into eax
    mov ebx,[ebp+c] //load the address into ebx
    bswap eax
     
    mov [ebx],eax //write the value into the address
    pop ebx
    pop eax
    pop ebp
    ret 8
    [/32-bit]
    

    代码2:4 Bytes Big Endian

    alloc(TypeName,256)
    alloc(ByteSize,4)
    alloc(ConvertRoutine,1024)
    alloc(ConvertBackRoutine,1024)
    
    TypeName:
    db '4 Bytes Big Endian',0
    
    ByteSize:
    dd 4
    
    //The convert routine should hold a routine that converts the data to an integer (in eax)
    //function declared as: stdcall int ConvertRoutine(unsigned char *input);
    //Note: Keep in mind that this routine can be called by multiple threads at the same time.
    ConvertRoutine:
    //jmp dllname.functionname
    [64-bit]
    //or manual:
    //parameters: (64-bit)
    //rcx=address of input
    xor eax,eax
    mov eax,[rcx] //eax now contains the bytes 'input' pointed to
    bswap eax //convert to big endian
    
    ret
    [/64-bit]
    
    [32-bit]
    //jmp dllname.functionname
    //or manual:
    //parameters: (32-bit)
    push ebp
    mov ebp,esp
    //[ebp+8]=input
    //example:
    mov eax,[ebp+8] //place the address that contains the bytes into eax
    mov eax,[eax] //place the bytes into eax so it's handled as a normal 4 byte value
    
    bswap eax
    
    pop ebp
    ret 4
    [/32-bit]
    
    //The convert back routine should hold a routine that converts the given integer back to a row of bytes (e.g when the user wats to write a new value)
    //function declared as: stdcall void ConvertBackRoutine(int i, unsigned char *output);
    ConvertBackRoutine:
    //jmp dllname.functionname
    //or manual:
    [64-bit]
    //parameters: (64-bit)
    //ecx=input
    //rdx=address of output
    //example:
    bswap ecx //convert the little endian input into a big endian input
    mov [rdx],ecx //place the integer the 4 bytes pointed to by rdx
    
    ret
    [/64-bit]
    
    [32-bit]
    //parameters: (32-bit)
    push ebp
    mov ebp,esp
    //[ebp+8]=input
    //[ebp+c]=address of output
    //example:
    push eax
    push ebx
    mov eax,[ebp+8] //load the value into eax
    mov ebx,[ebp+c] //load the address into ebx
    
    //convert the value to big endian
    bswap eax
    
    mov [ebx],eax //write the value into the address
    pop ebx
    pop eax
    
    pop ebp
    ret 8
    [/32-bit]
    

    代码3:2 Bytes Big Endian

    alloc(TypeName,256)
    alloc(ByteSize,4)
    alloc(ConvertRoutine,1024)
    alloc(ConvertBackRoutine,1024)
    
    TypeName:
    db '2 Bytes Big Endian',0
    
    ByteSize:
    dd 2
    
    //The convert routine should hold a routine that converts the data to an integer (in eax)
    //function declared as: stdcall int ConvertRoutine(unsigned char *input);
    //Note: Keep in mind that this routine can be called by multiple threads at the same time.
    ConvertRoutine:
    //jmp dllname.functionname
    [64-bit]
    //or manual:
    //parameters: (64-bit)
    //rcx=address of input
    xor eax,eax
    mov ax,[rcx] //eax now contains the bytes 'input' pointed to
    xchg ah,al //convert to big endian
    
    ret
    [/64-bit]
    
    [32-bit]
    //jmp dllname.functionname
    //or manual:
    //parameters: (32-bit)
    push ebp
    mov ebp,esp
    //[ebp+8]=input
    //example:
    mov eax,[ebp+8] //place the address that contains the bytes into eax
    mov ax,[eax] //place the bytes into eax so it's handled as a normal 4 byte value
    and eax,ffff //cleanup
    xchg ah,al //convert to big endian
    
    pop ebp
    ret 4
    [/32-bit]
    
    //The convert back routine should hold a routine that converts the given integer back to a row of bytes (e.g when the user wats to write a new value)
    //function declared as: stdcall void ConvertBackRoutine(int i, unsigned char *output);
    ConvertBackRoutine:
    //jmp dllname.functionname
    //or manual:
    [64-bit]
    //parameters: (64-bit)
    //ecx=input
    //rdx=address of output
    //example:
    xchg ch,cl //convert the little endian input into a big endian input
    mov [rdx],cx //place the integer the 4 bytes pointed to by rdx
    
    ret
    [/64-bit]
    
    [32-bit]
    //parameters: (32-bit)
    push ebp
    mov ebp,esp
    //[ebp+8]=input
    //[ebp+c]=address of output
    //example:
    push eax
    push ebx
    mov eax,[ebp+8] //load the value into eax
    mov ebx,[ebp+c] //load the address into ebx
    
    //convert the value to big endian
    xchg ah,al
    
    mov [ebx],ax //write the value into the address
    pop ebx
    pop eax
    
    pop ebp
    ret 8
    [/32-bit]
    

    搞定!试试效果:
    20231120_cheat_engine_2


    • (1):真正的谜底是“贵妃红”,有人听说过这种花么?
    • (2):另一版到张铁林出场就退了
    • (3):臭宝给这只小公猫起名“娜娜”。
    ❌
    ❌