Skip to content

从真实文件得到像素、PCM与带时间的视频帧 ​

上一节计算包长,本节实际编码、打开、重采样和解码媒体。先理解Python数组与文件上下文管理;不需要摄像头或模型密钥。单独建立Python3.12环境:

bash
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install 'Pillow==11.3.0' 'av==16.0.1' 'numpy==2.2.6' 'scipy==1.15.3'

坐标、采样与PTS分别保持什么 ​

图片先按EXIF定向再裁切,坐标也以定向后的图为基准;如果把裁切图的坐标直接标在原图上,引用位置会错。音频重采样要改变样本序列而非只改采样率标签。视频解码得到frame,传输/文件中通常是packet:一个packet不保证对应一张可展示图,编码器结束时还要flush缓存。

保存 media_files.py 并运行。所有输入均本地产生,临时目录退出后删除;视频采用真正的MPEG-4编码,未调用模型。

python
from pathlib import Path
from fractions import Fraction
import tempfile,wave
import numpy as np
from scipy.signal import resample_poly
from PIL import Image,ImageDraw,ImageOps
import av

with tempfile.TemporaryDirectory() as directory:
    root=Path(directory)
    # 合成图片有已知坐标真值,不依赖OCR模型猜测。
    source=Image.new('RGB',(320,180),'white')
    ImageDraw.Draw(source).rectangle((100,40,219,99),fill='red')
    source.save(root/'frame.png')
    with Image.open(root/'frame.png') as image:
        image=ImageOps.exif_transpose(image).convert('RGB')
        assert image.size==(320,180)
        box=(100,40,220,100)
        crop=image.crop(box).resize((240,120))
        assert crop.getpixel((10,10))==(255,0,0)
        # 返回裁切缩放图的点,映射回经过EXIF定向后的原图。
        x,y=20,20
        original=(box[0]+x/2,box[1]+y/2)
        assert original==(110,50)
    # 真实WAV文件:16 kHz -> 24 kHz,有低通滤波,保持时长。
    rate=16000
    signal=.2*np.sin(2*np.pi*440*np.arange(rate)/rate)
    pcm=(signal*32767).astype('<i2')
    with wave.open(str(root/'input.wav'),'wb') as wav:
        wav.setparams((1,2,rate,len(pcm),'NONE','not compressed'))
        wav.writeframes(pcm.tobytes())
    with wave.open(str(root/'input.wav'),'rb') as wav:
        assert (wav.getnchannels(),wav.getsampwidth(),wav.getframerate())==(1,2,16000)
        decoded=np.frombuffer(wav.readframes(wav.getnframes()),dtype='<i2').astype(float)/32768
    out=resample_poly(decoded,3,2)
    assert len(out)==24000 and len(decoded)/16000==len(out)/24000==1
    peak=np.fft.rfftfreq(len(out),1/24000)[np.argmax(abs(np.fft.rfft(out)))]
    assert abs(peak-440)<2
    encoded=(np.clip(out,-1,1)*32767).astype('<i2').tobytes()
    assert len(encoded)==48000
    # 真实压缩视频编码/解码;PTS而非帧到达时钟决定媒体时间。
    video=root/'clip.mkv'
    with av.open(str(video),'w') as container:
        stream=container.add_stream('mpeg4',rate=10)
        stream.width=160;stream.height=120;stream.pix_fmt='yuv420p'
        for i in range(10):
            pixels=np.full((120,160,3),i*20,dtype=np.uint8)
            frame=av.VideoFrame.from_ndarray(pixels,format='rgb24')
            frame.pts=i;frame.time_base=Fraction(1,10)
            for packet in stream.encode(frame):container.mux(packet)
        for packet in stream.encode():container.mux(packet)
    with av.open(str(video)) as container:
        timestamps=[float(frame.pts*frame.time_base) for frame in container.decode(video=0)]
    assert len(timestamps)==10 and timestamps==sorted(timestamps)
    assert abs(timestamps[-1]-.9)<.01
    print('pixels, PCM resampling and video PTS passed')

图片裁切范围120×60再放大2倍,所以裁切图点(20,20)回到原图(110,50)。音频从16000个采样变为24000个,时长仍为1秒,频谱峰仍约440Hz。若只把16000个采样的标签改为24000Hz,时长会缩成2/3秒、音高也改变;上采样不会创造原来没有的高频信息。resample_poly同时做滤波与插值,降采样不能直接每隔几个样本取一次。

最后得到10个解码帧,最后PTS约0.9秒。PTS乘各帧time_base才是媒体时间,不能假定整数PTS就是毫秒。真实视频可能可变帧率、缺PTS或时间线跳跃,应明确拒绝或采用容器允许的重建规则,不能以数组序号/固定fps冒充时间戳。

从感知到证据 ​

OCR输出保存文字、置信度、页/帧身份和坐标;表格保留行列与合并关系,不能只把所有字串在一起。图像embedding可以定位语义相似图,文字embedding可以定位字幕,两者融合前保持各自排名,不能直接相加原始分数。视频回答应引用文件版本与时间区间:某帧有物体并不证明整个片段持续存在。

把帧号、PTS、裁切框、原图尺寸和文档版本写进检索metadata;返回引用后验证坐标在边界内、时间落在原片段。OCR真实解析继续阅读文档摄取,融合机制见检索与重排。这里教媒体转为证据的接口,OCR识别率仍须用有标注图片评估。

像素数、音频秒数与解码后内存比压缩文件大小更能解释资源开销。限制解码尺寸、时长、并发和超时,失败时清理临时文件;背景屏幕与旁人声音同样属于采集范围。原始媒体、转录、裁切与embedding的删除要按同一个来源ID追踪。

练习 ​

把裁切结果改为原区域的3倍大小,求缩放图点(30,30)的原坐标。把音频重采样方向改为24kHz→16kHz,验证时长保持1秒、输出32000字节。给视频抽帧选择目标0.35秒,返回第一个PTS不小于目标的帧,解释为什么不能取第4个到达的网络包。

本章答案

用心记录每一步成长