Уменьшенная и сдвинутая картинка выходила в ролик с чёрными полями, и нижнюю дорожку закрывал не сам клип, а эти поля. Так же вело себя и уменьшенное видео. Между тем уменьшают клип ровно затем, чтобы под ним было видно другое. Теперь «не заполняет кадр» означает то же, что «не сплошной»: наравне с выбитым цветом и прозрачностью. Такой клип уходит в накладываемые — предпросмотр рисует его поверх нижнего силами плеера, сборка собирает участок слоями, где у верхнего слоя цепочка без полей и место, куда он встаёт. Класть не на что — поля остаются полями, как и были. Решение одно на оба пути: IsOverlayClip, и в нём TransformFillsFrame считает по тем же числам, что уходят в фильтр. Заодно две вещи, всплывшие по дороге: — у картинки своя ветка уплощения, и слои там не строились вовсе: клип с прозрачностью в окне накладывался, а в ролике нет. Теперь строятся. — в сборке ветка «готовая вставка» стояла раньше ветки слоёв, и кусок со слоями уходил в неё за несуществующей заготовкой. Заготовка для такого куска больше и не заказывается: это были бы минуты впустую. Проверка tests/imagecrop.sh получила второй случай: под картинкой видео, вокруг неё обязано быть видно его. На прежней сборке там чёрное.
181 lines
8.7 KiB
Bash
Executable file
181 lines
8.7 KiB
Bash
Executable file
#!/bin/sh
|
||
# Своё кадрирование картинки доходит до готового ролика.
|
||
#
|
||
# Окно «Кадрирование и поворот» показывало картинку уменьшенной и
|
||
# сдвинутой, а в предпросмотре и в ролике она шла во весь кадр. Причина
|
||
# была в уплощении: преобразование ставилось только обычному видео, а у
|
||
# картинки своя ветка, и там его не было вовсе. Кусок из картинки делался
|
||
# по жёсткой цепочке «вписать целиком плюс поля», про клип она ничего не
|
||
# знала.
|
||
#
|
||
# Проверяется сквозь всю дорогу: картинка делается на месте, кладётся
|
||
# одним клипом с уменьшением и смещением вправо, собирается секунда
|
||
# ролика. В готовом кадре смотрим на точки: слева картинки быть не
|
||
# должно, справа — должна.
|
||
#
|
||
# Запуск: tests/imagecrop.sh [<путь к nlvetool>]
|
||
set -e
|
||
cd "$(dirname "$0")/.."
|
||
TOOL=${1:-$PWD/tests/nlvetool}
|
||
WORK=$PWD/test/картинка
|
||
FF=${FFMPEG:-/home/chaiman/bin/ffmpeg}
|
||
|
||
[ -x "$TOOL" ] || { echo "нет $TOOL — собери: ./build.sh test"; exit 2; }
|
||
[ -x "$FF" ] || FF=ffmpeg
|
||
|
||
rm -rf "$WORK"
|
||
mkdir -p "$WORK"
|
||
|
||
# Картинка 900x1200, как у настоящей рекламной вертикали: сплошной цвет,
|
||
# чтобы по точке было видно, попала она в кадр или нет.
|
||
"$FF" -hide_banner -loglevel error -y -f lavfi -i color=c=green:s=900x1200 \
|
||
-frames:v 1 "$WORK/кадр.png"
|
||
|
||
python3 - "$WORK" <<'PY'
|
||
import json, sys, os
|
||
W = sys.argv[1]
|
||
кадр = {'valid': True, 'kind': 'video', 'has_video': True,
|
||
'width': 1920, 'height': 1080, 'fps_num': 30, 'fps_den': 1,
|
||
'video_codec': 'h264', 'pix_fmt': 'yuv420p',
|
||
'has_audio': True, 'audio_codec': 'aac',
|
||
'sample_rate': 48000, 'channels': 2, 'video_bitrate': 8000000}
|
||
проект = {
|
||
'version': 1, 'next_id': 100, 'master': кадр,
|
||
'pool': [{'id': 'm1', 'path': os.path.join(W, 'кадр.png'),
|
||
'normalized_path': '',
|
||
'info': {'valid': True, 'kind': 'image', 'has_video': True,
|
||
'video_codec': 'png', 'width': 900, 'height': 1200,
|
||
'fps_num': 25, 'fps_den': 1, 'pix_fmt': 'rgb24',
|
||
'duration': 0.0, 'has_audio': False}}],
|
||
'tracks': [
|
||
{'id': 't1', 'name': 'V1', 'kind': 'video', 'clips': [
|
||
{'id': 'c1', 'media': 'm1', 'kind': 'image',
|
||
'src_in': 0.0, 'tl_start': 0.0, 'duration': 1.0,
|
||
'edge_in': 'snap', 'edge_out': 'snap', 'group': '',
|
||
'volume': 1.0, 'opacity': 1.0, 'fade_in': 0.0, 'fade_out': 0.0,
|
||
# Уменьшить вдвое и сдвинуть вправо — как в окне кадрирования.
|
||
'transform': {'fit': 'fit', 'rotation': 0, 'flip_h': False,
|
||
'flip_v': False, 'zoom': 0.5, 'offset_x': 0.6,
|
||
'offset_y': 0.0},
|
||
'text': ''}]},
|
||
{'id': 't2', 'name': 'A1', 'kind': 'audio', 'clips': []}
|
||
],
|
||
'settings': {}
|
||
}
|
||
json.dump(проект, open(os.path.join(W, 'картинка.nlve'), 'w'),
|
||
ensure_ascii=False)
|
||
print(' картинка 900x1200, кадр проекта 1920x1080, увеличение 50%,'
|
||
' смещение вправо 60%')
|
||
PY
|
||
|
||
OUT=$WORK/ролик.mkv
|
||
"$TOOL" render "$WORK/картинка.nlve" "$OUT" >"$WORK/сборка.log" 2>&1 ||
|
||
{ echo " сборка не прошла, смотри $WORK/сборка.log"; tail -5 "$WORK/сборка.log"; exit 1; }
|
||
|
||
"$FF" -hide_banner -loglevel error -y -i "$OUT" -frames:v 1 "$WORK/готовый.png"
|
||
|
||
python3 - "$WORK" <<'PY'
|
||
import sys, os
|
||
from PIL import Image
|
||
W = sys.argv[1]
|
||
im = Image.open(os.path.join(W, 'готовый.png')).convert('RGB')
|
||
ш, в = im.size
|
||
print(' кадр ролика: %dx%d' % (ш, в))
|
||
|
||
def зелено(точка):
|
||
r, g, b = точка
|
||
return g > 80 and g > r + 40 and g > b + 40
|
||
|
||
# При увеличении 50% и смещении вправо 60% картинка занимает примерно
|
||
# правую пятую часть кадра по ширине и среднюю половину по высоте
|
||
# (цепочка сборки: scale=406:540,pad=1920:1080:1514:270). Поэтому слева и
|
||
# у самого верха обязано быть чёрное поле, а в правой части — картинка.
|
||
слева = im.getpixel((int(ш * 0.12), в // 2))
|
||
справа = im.getpixel((int(ш * 0.90), в // 2))
|
||
верх = im.getpixel((ш // 2, 6))
|
||
print(' слева %s, справа %s, вверху %s' % (слева, справа, верх))
|
||
|
||
плохо = 0
|
||
if зелено(слева):
|
||
print(' ПЛОХО: картинка слева — смещение не применилось')
|
||
плохо = 1
|
||
if not зелено(справа):
|
||
print(' ПЛОХО: справа картинки нет — она встала не туда')
|
||
плохо = 1
|
||
if зелено(верх):
|
||
print(' ПЛОХО: картинка достаёт до верха — уменьшение не применилось')
|
||
плохо = 1
|
||
if плохо:
|
||
raise SystemExit(1)
|
||
print(' ok: картинка уменьшена и сдвинута вправо, как в окне кадрирования')
|
||
PY
|
||
|
||
# Второй случай: под картинкой лежит видео. Тогда пустое место вокруг неё
|
||
# не чёрное поле, а это самое видео: клип, не заполняющий кадр, ложится
|
||
# поверх нижнего, а не подменяет его собой.
|
||
echo
|
||
echo " то же, но с видео на дорожке ниже"
|
||
"$FF" -hide_banner -loglevel error -y -f lavfi -i color=c=red:s=1920x1080:rate=30 \
|
||
-t 2 -c:v libx264 -pix_fmt yuv420p -g 15 "$WORK/низ.mp4"
|
||
|
||
python3 - "$WORK" <<'PY'
|
||
import json, os, sys
|
||
W = sys.argv[1]
|
||
d = json.load(open(os.path.join(W, 'картинка.nlve')))
|
||
d['pool'].append({'id': 'm2', 'path': os.path.join(W, 'низ.mp4'),
|
||
'normalized_path': '',
|
||
'info': {'valid': True, 'kind': 'video', 'has_video': True,
|
||
'video_codec': 'h264', 'width': 1920,
|
||
'height': 1080, 'fps_num': 30, 'fps_den': 1,
|
||
'pix_fmt': 'yuv420p', 'duration': 2.0,
|
||
'has_audio': False}})
|
||
низ = {'id': 'c2', 'media': 'm2', 'kind': 'video', 'src_in': 0.0,
|
||
'tl_start': 0.0, 'duration': 1.0, 'edge_in': 'snap',
|
||
'edge_out': 'snap', 'group': '', 'volume': 1.0, 'opacity': 1.0,
|
||
'fade_in': 0.0, 'fade_out': 0.0,
|
||
'transform': {'fit': 'fit', 'rotation': 0, 'flip_h': False,
|
||
'flip_v': False, 'zoom': 1.0, 'offset_x': 0.0,
|
||
'offset_y': 0.0},
|
||
'text': ''}
|
||
# Вторая видеодорожка идёт в списке после первой, то есть ниже неё.
|
||
d['tracks'].insert(1, {'id': 't3', 'name': 'V2', 'kind': 'video',
|
||
'clips': [низ]})
|
||
json.dump(d, open(os.path.join(W, 'поверх.nlve'), 'w'), ensure_ascii=False)
|
||
PY
|
||
|
||
OUT2=$WORK/поверх.mkv
|
||
"$TOOL" render "$WORK/поверх.nlve" "$OUT2" >"$WORK/сборка2.log" 2>&1 ||
|
||
{ echo " сборка не прошла, смотри $WORK/сборка2.log"; tail -5 "$WORK/сборка2.log"; exit 1; }
|
||
|
||
"$FF" -hide_banner -loglevel error -y -i "$OUT2" -frames:v 1 "$WORK/готовый2.png"
|
||
|
||
python3 - "$WORK" <<'PY'
|
||
import sys, os
|
||
from PIL import Image
|
||
W = sys.argv[1]
|
||
im = Image.open(os.path.join(W, 'готовый2.png')).convert('RGB')
|
||
ш, в = im.size
|
||
|
||
def зелено(т):
|
||
r, g, b = т
|
||
return g > 80 and g > r + 40 and g > b + 40
|
||
|
||
def красно(т):
|
||
r, g, b = т
|
||
return r > 80 and r > g + 40 and r > b + 40
|
||
|
||
слева = im.getpixel((int(ш * 0.12), в // 2))
|
||
справа = im.getpixel((int(ш * 0.90), в // 2))
|
||
print(' слева %s, справа %s' % (слева, справа))
|
||
|
||
плохо = 0
|
||
if not красно(слева):
|
||
print(' ПЛОХО: слева не нижнее видео — картинка закрыла его полями')
|
||
плохо = 1
|
||
if not зелено(справа):
|
||
print(' ПЛОХО: справа нет картинки')
|
||
плохо = 1
|
||
if плохо:
|
||
raise SystemExit(1)
|
||
print(' ok: картинка легла поверх видео, чёрных полей нет')
|
||
PY
|