nlve/tests/imagecrop.sh
4aiman 3d3d02368c Клип, не заполняющий кадр, ложится поверх нижнего, а не закрывает его
Уменьшенная и сдвинутая картинка выходила в ролик с чёрными полями, и
нижнюю дорожку закрывал не сам клип, а эти поля. Так же вело себя и
уменьшенное видео. Между тем уменьшают клип ровно затем, чтобы под ним
было видно другое.

Теперь «не заполняет кадр» означает то же, что «не сплошной»: наравне с
выбитым цветом и прозрачностью. Такой клип уходит в накладываемые —
предпросмотр рисует его поверх нижнего силами плеера, сборка собирает
участок слоями, где у верхнего слоя цепочка без полей и место, куда он
встаёт. Класть не на что — поля остаются полями, как и были.

Решение одно на оба пути: IsOverlayClip, и в нём TransformFillsFrame
считает по тем же числам, что уходят в фильтр.

Заодно две вещи, всплывшие по дороге:

— у картинки своя ветка уплощения, и слои там не строились вовсе: клип с
  прозрачностью в окне накладывался, а в ролике нет. Теперь строятся.
— в сборке ветка «готовая вставка» стояла раньше ветки слоёв, и кусок со
  слоями уходил в неё за несуществующей заготовкой. Заготовка для такого
  куска больше и не заказывается: это были бы минуты впустую.

Проверка tests/imagecrop.sh получила второй случай: под картинкой видео,
вокруг неё обязано быть видно его. На прежней сборке там чёрное.
2026-10-16 16:17:30 +03:00

181 lines
8.7 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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