🎬 第七章 FFmpeg命令行实现音视频特效及复杂滤镜应用
📄 一句话概括

crop=宽:高:x:y,x 与 y 是裁剪框左上角的坐标,不是中心点。 不写坐标默认居中,但尺寸与坐标都会被静默对齐到偶数,越界也不报错。

📝 本节和前面几节的关系

7.2 讲的是把画面整体缩放,这一节开始「只留一块」。 缩放改的是分辨率,裁剪改的是取景范围,两件事经常被混起来。

一、四个数说清裁剪

裁剪就一条命令,四个数:宽、高、左边留多少、上边留多少。

ffmpeg -i s7.mp4 -vf crop=80:44:40:22 out.mp4

crop=w:h:x:y。x、y 是裁剪框左上角在原图里的坐标,不是中心点。 不写 x、y 的时候它默认居中,这一点跟很多人想的不一样。

顺手记一个对比:scale 是把整幅画面改大小,crop 是原样切一块下来。 前者像素会被重算,后者像素是一个不动地搬过来的。

二、默认居中的实测

「默认居中」这句话对不对?把裁出来的图放回原图里找位置就知道了:

项 值


原图 160×90 裁多大 80×44 按公式算的居中 x=(160-80)/2=40 实测算出来 x=40,y=22

x 对上了,y 差了 1:(90-44)/2 = 23,实际落在第 22 行。 因为 yuv420p 的色度是横竖各减半存的,纵向坐标被对齐到了偶数,23 被就近取成 22。

差这一行在观感上完全看不出来,但只要拿它去做逐像素比对,就会差一行。 做视频指纹、做帧对齐的时候,这一行足以让两边永远对不上。

三、自己写的坐标也被抹平

那自己把坐标写成偶数行不行?先试三种写法:

写的 x:y 实际落点 说明


40:23 (40, 22) y 被取偶 41:23 (40, 22) x、y 都被取偶 40:22 (40, 22) 本来就合规

写 41 落在 40,写 23 落在 22。它不是四舍五入,是往下取偶数。 所以想精确到某一个像素,就得先把坐标自己改成偶数,别写完再指望它照办。

注意只有 yuv420p 会这样。换成 yuv444p 或者 rgb24 就没有色度减半这回事, 坐标想写奇数就写奇数。这个差别在做精确裁剪时很关键。

四、越界不报错,直接夹住

坐标算出界了会怎样?实测是不报错:

写的 裁出来 实际落点


80:44:120:60 80×44 (80, 46) 80:44:-40:-40 80×44 (0, 0) 120:80:100:100 120×80 (40, 10)

120:60 已经超出 160×90 的框,它没有报错,而是把起点挪回 (80, 46)。 80+80=160、46+44=90,正好贴住右下角。负坐标同理,一律夹到 0。

这个行为很危险:脚本里算错了坐标,它不会告诉你,只会给你一块位置不对的画面。 所以裁完一定要抽一帧看一眼,别只看命令有没有报错。

五、一句话记住 crop

crop=宽:高:左上角x:左上角y。 坐标是左上角不是中心,宽高和坐标都被对齐到偶数,越界会被静默夹住。

三种错法对应三种后果:尺寸超原图报 22、坐标越界被夹住、奇数被抹平。 只有第一种会响,后两种全靠你自己抽帧检查。

六、动手 Demo:把裁出来的图放回原图找位置

下面这段代码裁一次,再在整帧里逐点比一遍,找出它到底落在哪。 然后换成自己写坐标、写越界坐标,看结果怎么被抹平、被夹住。

import av
from fractions import Fraction

TB = Fraction(1, 25)
c0 = av.open(u"s7.mp4")
fr = next(c0.decode(video=0))
base = fr.to_ndarray(format=u"yuv420p")
full = fr.to_ndarray(format=u"rgb24")
W, H = fr.width, fr.height
c0.close()

def crop(args):
    g = av.filter.Graph()
    b = g.add_buffer(width=W, height=H, format=u"yuv420p", time_base=TB)
    f = g.add(u"crop", args)
    sk = g.add(u"buffersink")
    b.link_to(f)
    f.link_to(sk)
    g.configure()
    vf = av.VideoFrame.from_ndarray(base, format=u"yuv420p")
    vf.pts = 0
    vf.time_base = TB
    b.push(vf)
    return sk.pull().to_ndarray(format=u"rgb24")

def where(m):
    u"""把裁出来的图在整帧里找最像的位置。"""
    ch, cw = m.shape[0], m.shape[1]
    best = None
    for y in range(0, H - ch + 1):
        for x in range(0, W - cw + 1):
            d = abs(full[y:y + ch, x:x + cw].astype(int) - m.astype(int)).mean()
            if best is None or d < best[0]:
                best = (d, x, y)
    return best[1], best[2]

print(u"原图 %d×%d" % (W, H))
m = crop(u"80:44")
x, y = where(m)
print(u"crop=80:44 -> %d×%d 落在 (%d, %d)" % (m.shape[1], m.shape[0], x, y))
print(u"按公式算 (%d, %d)" % ((W - 80) // 2, (H - 44) // 2))

print(u"--- 自己写坐标")
for a in (u"80:44:40:23", u"80:44:41:23", u"80:44:40:22"):
    m = crop(a)
    x, y = where(m)
    print(u"%-12s -> (%d, %d)" % (a, x, y))

print(u"--- 越界")
for a in (u"80:44:120:60", u"80:44:-40:-40", u"120:80:100:100"):
    m = crop(a)
    x, y = where(m)
    print(u"%-14s -> %d×%d 落在 (%d, %d)"
          % (a, m.shape[1], m.shape[0], x, y))

实跑输出:

--------------------------------------------------------------------
7.4.1 使用 crop 实现裁剪
--------------------------------------------------------------------

**① 裁剪就一条命令,四个数:宽、高、左边留多少、上边留多少**

`ffmpeg -i s7.mp4 -vf crop=80:44:40:22 out.mp4`

**`crop=w:h:x:y`。x、y 是裁剪框左上角在原图里的坐标,不是中心点。**
**不写 x、y 的时候它默认居中,这一点跟很多人想的不一样。**

**② 实测默认居中到底居在哪**

  项                 值
  --------------------------------------
  原图               160×90
  裁多大             80×44
  按公式算的居中     x=(160-80)/2=40
  实测算出来         x=40
  实测算出来         y=22

**x 对上了,y 差了 1:(90-44)/2 = 23,实际落在第 22 行。**
**因为 yuv420p 的色度是横竖各减半存的,纵向坐标被对齐到了偶数,23 被就近取成 22。**

**③ 自己写 x、y 也一样会被抹平**

  写的 x:y     实际落点     差多少像素
  ----------------------------------------
  40:23        (40, 22)     1
  41:23        (40, 22)     2
  40:22        (40, 22)     0

**写 41 落在 40,写 23 落在 22。它不是四舍五入,是往下取偶数。**
**所以想精确到某一个像素,就得先把坐标自己改成偶数,别写完再指望它照办。**

**④ 越界不报错,直接夹住**

  写的               裁出来     实际落点
  ------------------------------------------
  80:44:120:60       80×44      (80, 46)
  80:44:-40:-40      80×44      (0, 0)
  120:80:100:100     120×80     (40, 10)

**`120:60` 已经超出 160×90 的框,它没有报错,而是把起点挪回 `(80, 46)`。**
**负坐标同理,一律夹到 0。**
**这个行为很危险:脚本里算错了坐标,它不会告诉你,只会给你一块位置不对的画面。**

**⑤ 一句话记住 crop**

**`crop=宽:高:左上角x:左上角y`,坐标是左上角不是中心,宽高和坐标都被对齐到偶数。**

第一行是原图尺寸,第二行是默认居中的实测落点 (40, 22), 第三行是公式算出来的 (40, 23)——差的就是那一行。

where() 用的办法是「逐点比差、取最小的那个偏移」, 所以它在原图里找出来的位置是像素级准确的,不是估的。

最后三行是越界:120:60 被夹到 (80, 46),-40:-40 被夹到 (0, 0), 120×80 那块被夹到 (40, 10)。三行一个错都没报。

30 秒实验

  1. 换尺寸:改成 crop=100:50,算一下默认落点,再跑代码验证。
  2. 试奇数:把坐标写成 41:23,看实际落点是不是 (40, 22)。
  3. 换像素格式:把 buffer 的 format 改成 yuv444p,看坐标还会不会被抹平。

常见误区

  1. 以为 x、y 是中心点。它们是左上角坐标,写错了画面整体偏移。
  2. 以为越界会报错。它会静默夹住,给你一块位置不对的画面。
  3. 以为坐标写奇数就精确到奇数。yuv420p 下会被取成偶数。
自测三问(点开对答案) 1. `crop=80:44` 的默认落点是多少? —— x=40、y=22。x 正好居中,y 比公式的 23 少一行,因为被取成偶数。 2. 坐标越界会怎样? —— 不报错,起点被夹到能放下这块画面的最大合法位置。 3. 什么时候不会取偶? —— 像素格式不是 yuv420p 时,比如 yuv444p 或 rgb24。

相关笔记