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 秒实验
- 换尺寸:改成
crop=100:50,算一下默认落点,再跑代码验证。 - 试奇数:把坐标写成
41:23,看实际落点是不是 (40, 22)。 - 换像素格式:把 buffer 的 format 改成
yuv444p,看坐标还会不会被抹平。
常见误区
- 以为 x、y 是中心点。它们是左上角坐标,写错了画面整体偏移。
- 以为越界会报错。它会静默夹住,给你一块位置不对的画面。
- 以为坐标写奇数就精确到奇数。yuv420p 下会被取成偶数。
自测三问(点开对答案)
1. `crop=80:44` 的默认落点是多少? —— x=40、y=22。x 正好居中,y 比公式的 23 少一行,因为被取成偶数。 2. 坐标越界会怎样? —— 不报错,起点被夹到能放下这块画面的最大合法位置。 3. 什么时候不会取偶? —— 像素格式不是 yuv420p 时,比如 yuv444p 或 rgb24。- ☐ (本节完,下一节 7.4.2 crop 参数说明)