เร่งคลิปจอเป็นช่วง ๆ แต่ให้นาฬิกาบนภาพเดินตามเวลาต้นฉบับ
เร่งคลิปทั้งเส้นด้วยตัวคูณเดียวเป็นเรื่องง่าย แต่พอต้องเร่งช่วงรอแรงกว่าช่วงลงมือ แล้วยังต้องให้คนดูรู้ว่าของจริงใช้เวลาไปเท่าไร ลำดับของตัวกรองในสายท่อจะกลายเป็นเรื่องเป็นเรื่องตายทันที
พอดีผมอัดจอไว้ตอนนั่งทำงานชิ้นหนึ่งยาว 12 นาทีกว่า อยากตัดเป็นไทม์แลปส์ให้เหลือราวสี่นาที แต่ไม่อยากเร่งทั้งเส้นด้วยตัวคูณเดียว เพราะครึ่งหนึ่งของคลิปคือหน้าจอนิ่ง ๆ ระหว่างรอประมวลผล ถ้าเร่งเท่ากันหมด ช่วงที่มีอะไรให้ดูจริงจะวูบผ่านไปเร็วเกินจนตามไม่ทัน ส่วนช่วงที่จอนิ่งก็ยังกินเวลาอยู่ดี ที่สำคัญคือผมอยากให้มีนาฬิกาบนภาพบอกเวลาจริงของต้นฉบับด้วย คนดูจะได้รู้ว่าที่เห็นวูบ ๆ นี่ของจริงกินเวลาไปกี่นาที
ตอนลงมือผมเจอกับดักที่ไม่คาดคิดคือ นาฬิกาบนภาพเดินเร็วตามความเร็วที่เร่ง ทั้งที่ต้องการให้มันเดินตามเวลาต้นฉบับ แก้อยู่หลายรอบกว่าจะรู้ว่าเป็นเรื่องลำดับตัวกรองล้วน ๆ บทความนี้ผมเลยเขียนเฉพาะเรื่องสายท่อของ filtergraph
ถ้ายังไม่เคยแตะ ffmpeg มาก่อน ให้อ่าน ใช้ Claude ตัดวิดีโอด้วยการพิมพ์สั่งเป็นภาษาไทย ก่อน ตอนนั้นว่าด้วยการสั่งตัด ย่อ รวม ใส่เพลง และเปิดการเร่งด้วยการ์ดจอ ตอนนี้ไม่เล่าซ้ำเรื่องพวกนั้นเลย เริ่มจากจุดที่คลิปอยู่ในมือแล้วและต้องเขียน filtergraph เอง
วัดก่อนว่าช่วงไหนจอนิ่ง ช่วงไหนมีอะไรเกิดขึ้น
ผมไม่ใช้วิธีนั่งดูแล้วจดเวลาเอง เพราะคลิปยาวสิบสองนาทีมีช่วงสลับไปมาหลายสิบครั้ง วิธีที่เร็วกว่าคือให้ ffmpeg วัดให้ ใช้ตัวกรอง tblend แบบ difference เทียบเฟรมติดกัน แล้วส่งเข้า signalstats เพื่ออ่านค่าความสว่างเฉลี่ยของภาพผลต่าง ถ้าจอไม่ขยับ ภาพผลต่างจะดำสนิทและค่าจะเข้าใกล้ศูนย์
ผมสุ่มแค่ 2 เฟรมต่อวินาทีก็พอ เพราะเราต้องการรู้แค่ว่าช่วงไหนขยับ ไม่ได้ต้องการความละเอียดระดับเฟรม แล้วย่อภาพลงด้วยเพื่อให้วัดเร็วขึ้นเยอะ
ffmpeg -hide_banner -i screen.mp4 \
-vf "fps=2,scale=480:-2,tblend=all_mode=difference,signalstats,metadata=print" \
-an -f null - 2> motion.log
ล็อกที่ได้เป็นแบบนี้ ค่า YAVG คือตัวที่ผมเอาไปใช้ตัดสิน
frame:1 pts:1 pts_time:0.5
lavfi.signalstats.YAVG=0.121
frame:2 pts:2 pts_time:1
lavfi.signalstats.YAVG=0.098
frame:3 pts:3 pts_time:1.5
lavfi.signalstats.YAVG=4.317
frame:4 pts:4 pts_time:2
lavfi.signalstats.YAVG=6.882
เส้นแบ่งที่ผมใช้คือ 0.8 ต่ำกว่านี้ถือว่านิ่ง สูงกว่านี้ถือว่ามีอะไรเกิดขึ้น ค่านี้ไม่ใช่ค่ามาตรฐาน ต้องเปิดล็อกดูของคลิปตัวเองแล้วเลือกเอง คลิปที่มีเคอร์เซอร์กะพริบหรือมีนาฬิกาในระบบเดินอยู่มุมจอจะมีพื้นค่าสูงกว่าปกติเสมอ
รวมช่วงย่อยให้เป็นก้อนก่อน ไม่งั้นได้ร้อยกว่าท่อน
ถ้าเอาผลดิบมาตัดเลยจะได้ช่วงยิบย่อยเป็นร้อย เพราะระหว่างพิมพ์คนเราหยุดคิดเป็นวินาที ๆ อยู่แล้ว ผมเลยใส่สามขั้นก่อนคือ ขยายช่วงทำงานออกด้านละหนึ่งวินาทีเผื่อบริบท ยุบช่วงที่สั้นกว่าสามวินาทีเข้ากับเพื่อนบ้าน แล้วรวมช่วงชนิดเดียวกันที่ติดกันเข้าด้วยกัน
THRESH, DILATE_S, MIN_SEG_S, DT = 0.8, 1.0, 3.0, 0.5
active = [v > THRESH for v in vals]
d = int(round(DILATE_S / DT))
act2 = active[:]
for i, a in enumerate(active):
if a:
for j in range(max(0, i - d), min(len(active), i + d + 1)):
act2[j] = True
active = act2
segs, i = [], 0
while i < len(active): # ตัดเป็นช่วงตามชนิด
j = i
while j < len(active) and active[j] == active[i]:
j += 1
segs.append([i * DT, j * DT, active[i]])
i = j
merged = [] # ยุบช่วงสั้นเข้ากับตัวก่อนหน้า
for s in segs:
if merged and (s[1] - s[0]) < MIN_SEG_S:
merged[-1][1] = s[1]
elif merged and merged[-1][2] == s[2]:
merged[-1][1] = s[1]
else:
merged.append(s[:])
ของผมเหลือ 49 ช่วงพอดี ซึ่งเป็นจำนวนที่ยังจัดการไหว ถ้าได้เกินร้อยให้ขยับ MIN_SEG_S ขึ้น เพราะทุกช่วงจะกลายเป็นหนึ่งสาขาใน filtergraph และเป็นหนึ่งอินพุตของ concat ยิ่งเยอะยิ่งกินหน่วยความจำตอนเรนเดอร์
ตัวคูณของช่วงทำงานตั้งเอง ตัวคูณของช่วงนิ่งให้คำนวณย้อนจากความยาวเป้าหมาย
ตรงนี้ผมใช้วิธีคิดง่าย ๆ คือช่วงทำงานผมอยากให้ดูรู้เรื่อง เลยตรึงไว้ที่ 3 เท่า ส่วนช่วงนิ่งปล่อยให้เป็นตัวแปรที่แก้สมการออกมาให้ได้ความยาวปลายทางตามที่ตั้งไว้ แล้วหนีบไม่ให้เกินขอบเขตที่ยอมรับได้
TARGET_OUT, ACTIVE_SPEED, IDLE_CLAMP = 230.0, 3.0, (6.0, 16.0)
active_total = sum(e - st for st, e, a in final if a)
idle_total = sum(e - st for st, e, a in final if not a)
out_active = active_total / ACTIVE_SPEED
remain = max(TARGET_OUT - out_active, 10.0)
idle_speed = min(max(idle_total / remain, IDLE_CLAMP[0]), IDLE_CLAMP[1])
est_out = out_active + idle_total / idle_speed
print(f"ACTIVE={ACTIVE_SPEED}x IDLE={idle_speed:.1f}x -> ~{est_out:.0f}s")
เหตุผลที่ต้องหนีบขอบบนไว้ที่ 16 เท่าคือ ถ้าปล่อยให้คำนวณอิสระแล้วคลิปมีช่วงนิ่งยาวมาก ตัวคูณจะพุ่งไปสี่ห้าสิบเท่า ผลคือช่วงนิ่งหายไปในพริบตาจนคนดูงงว่าภาพกระโดด ส่วนขอบล่างที่ 6 เท่ามีไว้กันอีกทาง คือถ้าคลิปแทบไม่มีช่วงนิ่งเลย ตัวคูณจะลงมาเท่ากับช่วงทำงานจนแยกไม่ออกว่าเร่งไปทำไม
หัวใจของเรื่อง drawtext ต้องมาก่อน setpts
ตรงนี้คือประเด็นหลักของทั้งบทความ ตัวกรอง drawtext ที่สั่งพิมพ์ %{pts:hms} มันอ่านค่าเวลาของเฟรม ณ จุดที่ตัวมันเองยืนอยู่ในสายท่อ ไม่ใช่เวลาของต้นฉบับ พอเราเอา setpts ไปหารเวลาลงเพื่อเร่งภาพ เฟรมที่ไหลออกมาจะมีเวลาใหม่ที่สั้นลงตามตัวคูณ ถ้า drawtext ยืนอยู่หลัง setpts มันก็อ่านเวลาใหม่นั้นไปพิมพ์ นาฬิกาบนภาพเลยเดินเร็วสามเท่าหกเท่าตามที่เราเร่ง
trim แล้ว setpts แล้วค่อย drawtext
นาฬิกาอ่านเวลาที่ถูกเร่งแล้ว คลิป 12 นาทีจะขึ้นตัวเลขวิ่งจบที่ราวสี่นาที เท่ากับบอกเวลาของคลิปปลายทาง ซึ่งคนดูเห็นจากแถบเล่นอยู่แล้ว ไม่มีประโยชน์
trim แล้ว drawtext แล้วค่อย setpts
นาฬิกาอ่านเวลาของเฟรมตอนที่ยังไม่ถูกเร่ง ตัวเลขบนภาพเลยกระโดดเป็นช่วง ๆ ตามตัวคูณ และจบที่ 12 นาทีกว่าตามความจริง คนดูเห็นได้ทันทีว่าช่วงไหนโดนเร่งแรง
ค่าที่พิมพ์ออกมาเป็นเวลาที่นับจากจุดเริ่มของ trim ในช่วงนั้น ไม่ใช่จากศูนย์ของไฟล์ต้นฉบับ ถ้าตัดต้นคลิปทิ้งไปสามวินาทีครึ่งแล้วอยากให้นาฬิกาตรงกับไฟล์เดิมเป๊ะ ต้องบวกค่าชดเชยเข้าไปเอง ผมไม่ได้บวก เพราะคลิปนี้ผมนับเวลาจากจุดที่เริ่มทำงานจริง ไม่ได้นับจากตอนที่กดอัด
ข้อความไทยต้องระบุฟอนต์เอง และเครื่องหมายทวิภาคต้องหลบ
สองเรื่องนี้ทำผมเสียเวลาไปพอสมควร เรื่องแรกคือ drawtext ไม่มีฟอนต์ไทยติดมาให้ ถ้าไม่ระบุ fontfile จะได้กล่องสี่เหลี่ยมเรียงกันเป็นแถว หรือไม่ก็ไม่ขึ้นอะไรเลย เรื่องที่สองคือเครื่องหมายทวิภาคเป็นตัวคั่นพารามิเตอร์ของตัวกรอง ถ้าเขียน %{pts:hms} ตรง ๆ ffmpeg จะอ่านว่าเป็นพารามิเตอร์ตัวใหม่แล้วฟ้องว่าไม่รู้จักตัวเลือกชื่อ hms
# นาฬิกาเวลาต้นฉบับ กับป้ายกำกับภาษาไทย
CLOCK = ("drawtext=fontfile=consolab.ttf:text='%{pts\\:hms}':fontsize=54:"
"fontcolor=white:box=1:[email protected]:boxborderw=9:x=36:y=30")
CLABEL = ("drawtext=fontfile=tahoma.ttf:text='เวลาจริงต้นฉบับ (ถูกเร่ง speed)':"
"fontsize=15:fontcolor=0x9fd0ff:box=1:[email protected]:"
"boxborderw=5:x=38:y=96")
OVL = CLOCK + "," + CLABEL
บนวินโดวส์พาธอย่าง C:\work\fonts\tahoma.ttf มีทั้งทวิภาคหลังชื่อไดรฟ์และแบ็กสแลช ซึ่งต้องหลบซ้อนกันหลายชั้นจนแทบไม่มีใครเขียนถูกในครั้งแรก ทางที่เสียเวลาน้อยที่สุดคือคัดลอกไฟล์ฟอนต์มาไว้ในโฟลเดอร์งาน แล้วอ้างชื่อไฟล์เปล่า ๆ พร้อมกับ cd เข้าโฟลเดอร์นั้นก่อนรัน ฟอนต์ตัวเลขกับฟอนต์ไทยแยกกันได้ ผมใช้ฟอนต์ความกว้างคงที่กับนาฬิกาเพื่อไม่ให้ตัวเลขขยับซ้ายขวาตอนวินาทีเปลี่ยน
อ้างอินพุตซ้ำไม่ได้ ต้อง split ออกเป็นกิ่งเท่าจำนวนช่วง
ใน filtergraph หนึ่งป้ายใช้ได้ครั้งเดียว จะเขียน [0:v]trim=... สี่สิบเก้าครั้งไม่ได้ ffmpeg จะฟ้องทันทีว่าป้ายถูกใช้ไปแล้ว ทางแก้คือ split ออกเป็นกิ่งตามจำนวนช่วงที่จะตัด แล้วแต่ละกิ่งค่อยไป trim ของตัวเอง
อีกเรื่องที่ต้องมีทุกกิ่งคือ fps กับ format ก่อนเข้า concat ถ้าปล่อยให้แต่ละกิ่งจบด้วยอัตราเฟรมหรือรูปแบบพิกเซลไม่เท่ากัน ผลคือภาพกระตุกตรงรอยต่อ หรือไม่ก็ได้ข้อความฟ้องตรง ๆ ว่าอินพุตของ concat ไม่ตรงกัน
K = len(final)
parts = ["[0:v]split=%d%s" % (K, "".join(f"[s{k}]" for k in range(K)))]
for k, (st, e, a) in enumerate(final):
spd = ACTIVE_SPEED if a else idle_speed
parts.append(f"[s{k}]trim=start={st:.3f}:end={e:.3f},{OVL},"
f"setpts=(PTS-STARTPTS)/{spd:.4f},fps=30,format=yuv420p[v{k}]")
graph = ";\n".join(parts) + ";\n" + \
"".join(f"[v{k}]" for k in range(K)) + f"concat=n={K}:v=1:a=0[outv]"
open("filtergraph.txt", "w", encoding="utf-8").write(graph)
หน้าตาของบรรทัดที่ได้ออกมาเป็นแบบนี้ สังเกตลำดับให้ดีคือ trim แล้ว drawtext สองตัว แล้วค่อย setpts แล้วปิดท้ายด้วย fps กับ format
[0:v]split=49[s0][s1][s2]...[s48];
[s0]trim=start=3.500:end=17.500,drawtext=fontfile=consolab.ttf:text='%{pts\:hms}'
:fontsize=54:fontcolor=white:box=1:[email protected]:boxborderw=9:x=36:y=30,
drawtext=fontfile=tahoma.ttf:text='เวลาจริงต้นฉบับ (ถูกเร่ง speed)':fontsize=15
:fontcolor=0x9fd0ff:box=1:[email protected]:boxborderw=5:x=38:y=96,
setpts=(PTS-STARTPTS)/3.0000,fps=30,format=yuv420p[v0];
[s1]trim=start=17.500:end=20.500,...,setpts=(PTS-STARTPTS)/6.0000,fps=30,
format=yuv420p[v1];
...
[v0][v1][v2]...[v48]concat=n=49:v=1:a=0[outv]
setpts=(PTS-STARTPTS)/N ต้องลบ STARTPTS ออกก่อนหารเสมอ เพราะหลัง trim เวลาของเฟรมยังนับต่อจากต้นไฟล์อยู่ ถ้าหารตรง ๆ โดยไม่ลบฐานออก ช่วงท้าย ๆ ของคลิปจะได้เวลาเริ่มต้นที่ไม่ใช่ศูนย์ แล้ว concat จะต่อกันเพี้ยน
กราฟยาวเกินบรรทัดคำสั่ง ให้อ่านจากไฟล์
กราฟของผมยาวสองหมื่นกว่าตัวอักษร ยัดลงบรรทัดคำสั่งไม่ไหวแน่นอน โดยเฉพาะบนวินโดวส์ที่มีเพดานความยาวบรรทัด ทางออกคือเขียนกราฟลงไฟล์แล้วให้ ffmpeg อ่านเอา
cd /c/work/video
ffmpeg -hide_banner -y -i screen.mp4 \
-filter_complex_script filtergraph.txt -map "[outv]" \
-c:v h264_nvenc -preset p5 -rc vbr -cq 23 -movflags +faststart \
timelapse.mp4
การเข้ารหัสด้วย h264_nvenc ทำให้รอบนี้จบเร็วกว่าเดิมหลายเท่า งานนี้ตัวถอดรหัสกับตัวกรองทำงานบนซีพียูอยู่แล้ว การ์ดจอช่วยได้เฉพาะขั้นเข้ารหัส แต่แค่ขั้นเดียวก็ช่วยได้เยอะเพราะเป็นขั้นที่หนักที่สุด ค่า cq 23 ผมว่ากำลังพอดีสำหรับคลิปหน้าจอที่มีตัวหนังสือเยอะ ถ้าตั้งสูงกว่านี้ตัวอักษรเล็ก ๆ จะเริ่มเลอะ
ตอนประกอบร่างสุดท้ายผมเอาการ์ดเปิดกับการ์ดปิดมาต่อหัวท้าย ตรงนี้ต้องบังคับให้ทุกอินพุตมีขนาด อัตราเฟรม และรูปแบบพิกเซลตรงกันอีกรอบ เพราะไฟล์ภาพนิ่งกับไฟล์วิดีโอมันคนละชนิดกันโดยธรรมชาติ
ffmpeg -hide_banner -y \
-loop 1 -t 4 -i card_intro.png \
-i timelapse.mp4 \
-loop 1 -t 4 -i card_outro.png \
-filter_complex "[0:v]scale=1280:720,fps=30,format=yuv420p[a];\
[1:v]scale=1280:720,fps=30,drawtext=fontfile=tahoma.ttf:text='example.com':\
fontsize=23:fontcolor=white:box=1:[email protected]:boxborderw=9:x=38:y=h-52,\
format=yuv420p[b];\
[2:v]scale=1280:720,fps=30,format=yuv420p[c];\
[a][b][c]concat=n=3:v=1[outv]" \
-map "[outv]" -c:v h264_nvenc -preset p5 -rc vbr -cq 23 -movflags +faststart \
final.mp4
echo "EXIT=$?"
สรุปกับดักที่ผมเจอ เรียงตามลำดับที่เสียเวลาให้มากที่สุด
- drawtext อยู่หลัง setpts นาฬิกาเดินตามเวลาที่ถูกเร่ง ย้ายมาไว้ก่อน setpts แล้วจบ
- ไม่ได้ระบุ fontfile ข้อความไทยกลายเป็นกล่องสี่เหลี่ยม ต้องชี้ไฟล์ฟอนต์เองทุกครั้ง
- ทวิภาคใน %{pts:hms} ไม่ได้หลบ ffmpeg ฟ้องว่าไม่รู้จักตัวเลือก ต้องใส่แบ็กสแลชหน้าทวิภาค
- อ้าง [0:v] ซ้ำหลายครั้ง ต้อง split ออกเป็นกิ่งเท่าจำนวนช่วงก่อนเสมอ
- ลืม fps กับ format ในบางกิ่ง concat ฟ้องหรือภาพกระตุกตรงรอยต่อ ต้องใส่ให้ครบทุกกิ่ง
- setpts หารโดยไม่ลบ STARTPTS ช่วงท้ายเวลาเริ่มไม่ใช่ศูนย์ ต่อกันแล้วเพี้ยน
- ยัดกราฟลงบรรทัดคำสั่ง พอยาวหลักหมื่นตัวอักษรต้องย้ายไปไฟล์แล้วใช้ filter_complex_script
ส่วนตัวผมว่าสิ่งที่ทำให้ไทม์แลปส์แบบนี้ดูรู้เรื่องกว่าการเร่งทั้งเส้น ไม่ใช่ความเนียนของภาพ แต่เป็นนาฬิกามุมจอนี่แหละ เพราะพอตัวเลขกระโดดจากนาทีที่สองไปนาทีที่ห้าในสองวินาที คนดูจะเข้าใจเองทันทีว่าช่วงนั้นคือช่วงนั่งรอ ไม่ต้องมีคำบรรยายอะไรเพิ่ม ใครจะเอาไปปรับตัวคูณหรือเปลี่ยนตัวเข้ารหัสตามเครื่องตัวเองก็ตามสะดวกครับ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย