Клип, не заполняющий кадр, ложится поверх нижнего, а не закрывает его
Уменьшенная и сдвинутая картинка выходила в ролик с чёрными полями, и нижнюю дорожку закрывал не сам клип, а эти поля. Так же вело себя и уменьшенное видео. Между тем уменьшают клип ровно затем, чтобы под ним было видно другое. Теперь «не заполняет кадр» означает то же, что «не сплошной»: наравне с выбитым цветом и прозрачностью. Такой клип уходит в накладываемые — предпросмотр рисует его поверх нижнего силами плеера, сборка собирает участок слоями, где у верхнего слоя цепочка без полей и место, куда он встаёт. Класть не на что — поля остаются полями, как и были. Решение одно на оба пути: IsOverlayClip, и в нём TransformFillsFrame считает по тем же числам, что уходят в фильтр. Заодно две вещи, всплывшие по дороге: — у картинки своя ветка уплощения, и слои там не строились вовсе: клип с прозрачностью в окне накладывался, а в ролике нет. Теперь строятся. — в сборке ветка «готовая вставка» стояла раньше ветки слоёв, и кусок со слоями уходил в неё за несуществующей заготовкой. Заготовка для такого куска больше и не заказывается: это были бы минуты впустую. Проверка tests/imagecrop.sh получила второй случай: под картинкой видео, вокруг неё обязано быть видно его. На прежней сборке там чёрное.
This commit is contained in:
parent
ffd105a8c0
commit
3d3d02368c
6 changed files with 193 additions and 17 deletions
25
README.md
25
README.md
|
|
@ -527,7 +527,19 @@ Linux потом искала по нему `.dll`.
|
|||
Два клипа **на одной дорожке**, наползшие друг на друга, дают переход:
|
||||
один плавно перетекает в другой. На разных дорожках перекрытие означает
|
||||
не переход, а наложение — там по-прежнему побеждает верхний, если он
|
||||
непрозрачен.
|
||||
сплошной.
|
||||
|
||||
Сплошным клип перестаёт быть по трём причинам: выбит цвет, задана
|
||||
прозрачность или **он не заполняет кадр**. Последнее — то же самое,
|
||||
сказанное иначе: уменьшенный или сдвинутый клип занимает часть кадра, а
|
||||
вокруг остаётся пустое место, которое сборка обкладывает чёрными полями.
|
||||
Поле непрозрачно, и нижнюю дорожку закрывает не сам клип, а его поля —
|
||||
поэтому такой клип кладётся поверх нижнего, а не вместо него. Уменьшают
|
||||
клип ровно затем, чтобы увидеть под ним другое. Класть не на что —
|
||||
остаются поля.
|
||||
|
||||
Отсюда же берётся картинка в углу кадра и два видео рядом: достаточно
|
||||
уменьшить и сдвинуть клипы на разных дорожках.
|
||||
|
||||
В месте наезда под указателем два клипа. Первый щелчок берёт верхний,
|
||||
повторный по тому же месту переводит выделение на следующий, после
|
||||
|
|
@ -713,6 +725,11 @@ Linux потом искала по нему `.dll`.
|
|||
«Кадрирование и поворот». Поэтому в кэше лежит столько заготовок,
|
||||
сколько разных кадрирований у этой картинки, и предпросмотр с роликом
|
||||
показывают одно и то же.
|
||||
|
||||
Заготовка нужна не всегда: если картинка не заполняет кадр, а под ней
|
||||
что-то лежит, участок собирается наложением, и чёрным полям взяться
|
||||
неоткуда. Заготовка с полями остаётся для случая, когда класть не на
|
||||
что.
|
||||
3. Границы кусков кладутся на кадровую сетку проекта: между кадрами
|
||||
времени нет, а звук набирается с точностью до отсчёта, и доли кадра
|
||||
разводят их в стороны.
|
||||
|
|
@ -1050,8 +1067,10 @@ Wine с новой библиотекой не грузилось ничего.
|
|||
|
||||
Отдельно от них `tests/imagecrop.sh` — сквозная, без окна: делает
|
||||
картинку, кладёт её клипом с уменьшением и смещением, собирает секунду
|
||||
ролика и смотрит на точки готового кадра. Ей нужны `tests/nlvetool`
|
||||
(`./build.sh test`), ffmpeg и python с PIL.
|
||||
ролика и смотрит на точки готового кадра. Случая два: класть не на что —
|
||||
вокруг картинки чёрные поля; под картинкой видео — вокруг неё видно его,
|
||||
а не поля. Ей нужны `tests/nlvetool` (`./build.sh test`), ffmpeg и python
|
||||
с PIL.
|
||||
|
||||
Последним восьми нужен монтаж в `test/поток/`: `длинный-фейд.nlve` — один
|
||||
клип с затуханием секунд на семьдесят (такой кусок сводится около
|
||||
|
|
|
|||
|
|
@ -969,8 +969,14 @@ begin
|
|||
{ Сгенерированные вставки берутся из файла, который заведомо длиннее
|
||||
нужного: так одна и та же чёрная вставка годится для дыр любой
|
||||
длины. Значит вырезать надо и из них, но притягивать к опорному
|
||||
кадру нечего — они начинаются с нуля, а ноль всегда опорный. }
|
||||
if (FSegments[I].Kind <> skVideo) and (FSegments[I].SubFile = '') then
|
||||
кадру нечего — они начинаются с нуля, а ноль всегда опорный.
|
||||
|
||||
Кусок со слоями сюда не попадает, какого бы он ни был вида:
|
||||
заготовки у него нет и быть не может, он собирается наложением
|
||||
ниже. Картинка, уменьшенная и положенная поверх нижнего видео, —
|
||||
как раз такой случай. }
|
||||
if (FSegments[I].Kind <> skVideo) and (FSegments[I].SubFile = '') and
|
||||
(Length(FSegments[I].Layers) = 0) then
|
||||
begin
|
||||
Dst := PartDst('a', I);
|
||||
Rep.Index := I;
|
||||
|
|
|
|||
|
|
@ -224,6 +224,10 @@ implementation
|
|||
uses
|
||||
Math, md5;
|
||||
|
||||
{ Объявлено заранее: уплощение решает по этому признаку, класть ли клип
|
||||
поверх нижнего, а само правило описано ниже, рядом с предпросмотром. }
|
||||
function IsOverlayClip(AProject: TProject; AClip: TClip): Boolean; forward;
|
||||
|
||||
const
|
||||
{ Длительность округляется до миллисекунд: иначе крошечное расхождение
|
||||
в дробной части плодило бы почти одинаковые файлы. }
|
||||
|
|
@ -908,6 +912,25 @@ begin
|
|||
AProject.Master.Width, AProject.Master.Height);
|
||||
Seg.GenFilter := Req.Filter;
|
||||
Seg.FilePath := GeneratedPath(AProject.CacheDir, Req);
|
||||
|
||||
{ Картинка, не заполняющая кадр, ложится поверх нижнего видео, а не
|
||||
подменяет его собой. Иначе нижнюю дорожку закрывают не картинкой,
|
||||
а её чёрными полями: сам кусок — сплошной кадр проекта, в котором
|
||||
картинка занимает свой прямоугольник, а остальное залито чёрным.
|
||||
|
||||
Слои считает тот же код, что и для полупрозрачных клипов: у
|
||||
верхнего слоя берётся цепочка без полей и место, куда он встаёт. }
|
||||
Under := nil;
|
||||
if IsOverlayClip(AProject, Winner) then
|
||||
Under := BelowAt(Winner, Mid);
|
||||
if Under <> nil then
|
||||
begin
|
||||
SetLength(Seg.Layers, 2);
|
||||
Seg.Layers[0] := LayerOf(Under, SegStart, 1, 1);
|
||||
Seg.Layers[1] := LayerOf(Winner, SegStart,
|
||||
Winner.Opacity, Winner.Opacity);
|
||||
Seg.Transcode := True;
|
||||
end;
|
||||
end
|
||||
else
|
||||
begin
|
||||
|
|
@ -970,11 +993,10 @@ begin
|
|||
|
||||
Partner := OverlapPartner(Winner, Mid);
|
||||
Under := nil;
|
||||
{ Ключ работает так же, как полупрозрачность: клип перестаёт быть
|
||||
сплошным, и участок приходится собирать вместе с тем, что под
|
||||
ним. }
|
||||
if (Partner = nil) and
|
||||
((Winner.Opacity < 0.999) or Winner.ColorKey.Enabled) then
|
||||
{ Ключ, прозрачность и неполный кадр — три способа перестать быть
|
||||
сплошным. В любом из них участок собирается вместе с тем, что
|
||||
лежит под ним: иначе нижнее видео закроют чёрные поля. }
|
||||
if (Partner = nil) and IsOverlayClip(AProject, Winner) then
|
||||
Under := BelowAt(Winner, Mid);
|
||||
|
||||
{ Ручное затухание: кусок попал в зону появления или ухода. Слой
|
||||
|
|
@ -1123,6 +1145,10 @@ begin
|
|||
begin
|
||||
if ASegments[I].Kind = skVideo then
|
||||
Continue;
|
||||
{ Кусок собирается слоями — заготовка ему не нужна, и делать её
|
||||
значило бы считать минуты впустую. }
|
||||
if Length(ASegments[I].Layers) > 0 then
|
||||
Continue;
|
||||
Path := ASegments[I].FilePath;
|
||||
if (Path = '') or FileExists(Path) then
|
||||
Continue;
|
||||
|
|
@ -1343,12 +1369,33 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
{ Клип кладётся поверх, а не побеждает целиком, если он не сплошной:
|
||||
с выбитым цветом или полупрозрачный. }
|
||||
function IsOverlayClip(AClip: TClip): Boolean;
|
||||
{ Клип кладётся поверх, а не побеждает целиком, если он не сплошной.
|
||||
|
||||
Несплошных случая три: выбитый цвет, прозрачность и клип, который не
|
||||
заполняет кадр. Последний — то же самое, только другими словами:
|
||||
уменьшенная или сдвинутая картинка занимает часть кадра, а остальное
|
||||
сборка обкладывает чёрными полями. Поле непрозрачно, и нижнее видео
|
||||
закрывает собой не картинка, а её поля. Человек же, уменьшая клип,
|
||||
хочет увидеть под ним то, что на дорожке ниже, — иначе зачем уменьшать.
|
||||
|
||||
Если под клипом ничего нет, поля остаются полями: класть не на что. }
|
||||
function IsOverlayClip(AProject: TProject; AClip: TClip): Boolean;
|
||||
var
|
||||
Item: TMediaItem;
|
||||
begin
|
||||
Result := (AClip <> nil) and (AClip.Kind in [ckVideo, ckImage]) and
|
||||
(AClip.ColorKey.Enabled or (AClip.Opacity < 0.999));
|
||||
Result := False;
|
||||
if (AClip = nil) or not (AClip.Kind in [ckVideo, ckImage]) then
|
||||
Exit;
|
||||
if AClip.ColorKey.Enabled or (AClip.Opacity < 0.999) then
|
||||
Exit(True);
|
||||
if AProject = nil then
|
||||
Exit;
|
||||
Item := AProject.FindMedia(AClip.MediaId);
|
||||
if Item = nil then
|
||||
Exit;
|
||||
Result := not TransformFillsFrame(AClip.Transform,
|
||||
Item.Info.Width, Item.Info.Height,
|
||||
AProject.Master.Width, AProject.Master.Height);
|
||||
end;
|
||||
|
||||
function FlattenPreview(AProject: TProject;
|
||||
|
|
@ -1407,7 +1454,7 @@ begin
|
|||
for C := 0 to Track.Count - 1 do
|
||||
begin
|
||||
Clip := Track[C];
|
||||
if not IsOverlayClip(Clip) then
|
||||
if not IsOverlayClip(AProject, Clip) then
|
||||
Continue;
|
||||
Item := AProject.FindMedia(Clip.MediaId);
|
||||
if Item = nil then
|
||||
|
|
@ -1445,7 +1492,7 @@ begin
|
|||
Continue;
|
||||
for J := 0 to Above.Count - 1 do
|
||||
{ Клип с ключом или прозрачностью сам ничего не закрывает. }
|
||||
if not IsOverlayClip(Above[J]) then
|
||||
if not IsOverlayClip(AProject, Above[J]) then
|
||||
CutRange(RangeA, RangeB, RangeCount,
|
||||
Above[J].TlStart, Above[J].TlEnd);
|
||||
end;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,15 @@ function TransformIsIdentity(const AT: TMediaTransform;
|
|||
function TransformFilter(const AT: TMediaTransform;
|
||||
ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer): string;
|
||||
|
||||
{ Заполняет ли клип кадр проекта целиком.
|
||||
|
||||
Нет — значит вокруг него остаётся пустое место, и сборка обкладывает
|
||||
его чёрными полями. Поле непрозрачно и закрывает собой всё, что лежит
|
||||
на дорожках ниже, поэтому такой клип надо не подставлять вместо
|
||||
нижнего, а класть поверх него. }
|
||||
function TransformFillsFrame(const AT: TMediaTransform;
|
||||
ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer): Boolean;
|
||||
|
||||
{ Цепочка для предпросмотра: вписать любой кадр в кадр проекта.
|
||||
|
||||
От TransformFilter отличается двумя вещами. Во-первых, размеры исходника
|
||||
|
|
@ -406,6 +415,26 @@ begin
|
|||
Result := Chain;
|
||||
end;
|
||||
|
||||
function TransformFillsFrame(const AT: TMediaTransform;
|
||||
ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer): Boolean;
|
||||
var
|
||||
X, Y, W, H: Integer;
|
||||
begin
|
||||
{ Размеры неизвестны — считаем, что заполняет: иначе каждый клип без
|
||||
разобранного файла объявлялся бы накладываемым. }
|
||||
Result := True;
|
||||
if (ASrcWidth <= 0) or (ASrcHeight <= 0) or
|
||||
(ADstWidth <= 0) or (ADstHeight <= 0) then
|
||||
Exit;
|
||||
if BuildTransform(AT, ASrcWidth, ASrcHeight, ADstWidth, ADstHeight,
|
||||
X, Y, W, H) = '' then
|
||||
Exit;
|
||||
{ Считаем по тем же числам, что уходят в фильтр: именно их ffmpeg и
|
||||
обкладывает полями, и ровно эти поля закрывают нижнее видео. }
|
||||
Result := (X <= 0) and (Y <= 0) and
|
||||
(X + W >= ADstWidth) and (Y + H >= ADstHeight);
|
||||
end;
|
||||
|
||||
function TransformFilter(const AT: TMediaTransform;
|
||||
ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer): string;
|
||||
var
|
||||
|
|
|
|||
|
|
@ -109,3 +109,73 @@ 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
|
||||
|
|
|
|||
|
|
@ -573,6 +573,11 @@ begin
|
|||
else
|
||||
KindName := 'видео';
|
||||
end;
|
||||
{ Слои видно сразу: по ним отличается кусок, который собирается
|
||||
наложением, от куска, который просто копируется. }
|
||||
if Length(ASegments[I].Layers) > 0 then
|
||||
KindName := KindName + Format(' (слоёв: %d)',
|
||||
[Length(ASegments[I].Layers)]);
|
||||
WriteLn(Format(' [%d] %s из %s длина %s',
|
||||
[I, KindName, SecondsToStr(ASegments[I].SrcStart),
|
||||
SecondsToStr(ASegments[I].Duration)]));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue