bladeworks 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bladeworks/__init__.py +56 -0
- bladeworks/__main__.py +4 -0
- bladeworks/cli.py +728 -0
- bladeworks/core/__init__.py +34 -0
- bladeworks/core/animation.py +893 -0
- bladeworks/core/animation_execution.py +817 -0
- bladeworks/core/audio_enhancements.py +673 -0
- bladeworks/core/audio_execution.py +1848 -0
- bladeworks/core/audio_ir.py +1971 -0
- bladeworks/core/basic_effects.py +168 -0
- bladeworks/core/bindings.py +196 -0
- bladeworks/core/capabilities.py +257 -0
- bladeworks/core/cohort_effects.py +702 -0
- bladeworks/core/color.py +324 -0
- bladeworks/core/compiler.py +3142 -0
- bladeworks/core/composition_diagnostics.py +629 -0
- bladeworks/core/composition_ir.py +3154 -0
- bladeworks/core/composition_planning.py +708 -0
- bladeworks/core/composition_shadow.py +687 -0
- bladeworks/core/compositor.py +1186 -0
- bladeworks/core/effect_parameter_semantics.py +165 -0
- bladeworks/core/effect_parameters.py +193 -0
- bladeworks/core/errors.py +21 -0
- bladeworks/core/filter_text.py +48 -0
- bladeworks/core/geometry.py +1711 -0
- bladeworks/core/group_surface.py +664 -0
- bladeworks/core/keyer.py +280 -0
- bladeworks/core/masks.py +453 -0
- bladeworks/core/missing_media.py +130 -0
- bladeworks/core/model.py +985 -0
- bladeworks/core/multicam_execution.py +496 -0
- bladeworks/core/output_color.py +52 -0
- bladeworks/core/parser.py +1257 -0
- bladeworks/core/pixel_domains.py +433 -0
- bladeworks/core/render_profile.py +311 -0
- bladeworks/core/render_sources.py +475 -0
- bladeworks/core/report.py +118 -0
- bladeworks/core/retime.py +967 -0
- bladeworks/core/retime_execution.py +1515 -0
- bladeworks/core/spatial_intrinsics.py +1749 -0
- bladeworks/core/story_containers.py +524 -0
- bladeworks/core/story_ir.py +1042 -0
- bladeworks/core/text.py +1032 -0
- bladeworks/core/text_templates.py +934 -0
- bladeworks/data/FCPXML_RENDER_CAPABILITIES.yaml +1797 -0
- bladeworks/effects/__init__.py +1 -0
- bladeworks/effects/contract.py +162 -0
- bladeworks/examples/__init__.py +80 -0
- bladeworks/examples/color.fcpxmld/Info.fcpxml +17 -0
- bladeworks/examples/color.fcpxmld/Media/a.mp4 +0 -0
- bladeworks/examples/crop_or_transition.fcpxmld/Info.fcpxml +13 -0
- bladeworks/examples/crop_or_transition.fcpxmld/Media/a.mp4 +0 -0
- bladeworks/examples/crop_or_transition.fcpxmld/Media/b.mp4 +0 -0
- bladeworks/examples/manual_qa/generate_2h_qa_fixtures.py +237 -0
- bladeworks/examples/manual_qa/missing_media_lab.fcpxmld/Info.fcpxml +28 -0
- bladeworks/examples/manual_qa/missing_media_lab.fcpxmld/Media/night_signal.mp4 +0 -0
- bladeworks/examples/manual_qa/portrait_social.fcpxmld/Info.fcpxml +44 -0
- bladeworks/examples/manual_qa/portrait_social.fcpxmld/Media/city_grid.mp4 +0 -0
- bladeworks/examples/manual_qa/portrait_social.fcpxmld/Media/golden_hour.mp4 +0 -0
- bladeworks/examples/manual_qa/studio_showcase.fcpxmld/Info.fcpxml +113 -0
- bladeworks/examples/manual_qa/studio_showcase.fcpxmld/Media/city_grid.mp4 +0 -0
- bladeworks/examples/manual_qa/studio_showcase.fcpxmld/Media/golden_hour.mp4 +0 -0
- bladeworks/examples/manual_qa/studio_showcase.fcpxmld/Media/night_signal.mp4 +0 -0
- bladeworks/examples/single_clip.fcpxmld/Info.fcpxml +9 -0
- bladeworks/examples/single_clip.fcpxmld/Media/a.mp4 +0 -0
- bladeworks/examples/spine_and_lane.fcpxmld/Info.fcpxml +13 -0
- bladeworks/examples/spine_and_lane.fcpxmld/Media/a.mp4 +0 -0
- bladeworks/examples/spine_and_lane.fcpxmld/Media/b.mp4 +0 -0
- bladeworks/examples/transform.fcpxmld/Info.fcpxml +11 -0
- bladeworks/examples/transform.fcpxmld/Media/a.mp4 +0 -0
- bladeworks/executor.py +293 -0
- bladeworks/preview/__init__.py +11 -0
- bladeworks/preview/application.py +84 -0
- bladeworks/preview/audio.py +104 -0
- bladeworks/preview/capabilities.py +457 -0
- bladeworks/preview/contracts.py +157 -0
- bladeworks/preview/events.py +55 -0
- bladeworks/preview/export.py +57 -0
- bladeworks/preview/history.py +197 -0
- bladeworks/preview/main.py +19 -0
- bladeworks/preview/media.py +23 -0
- bladeworks/preview/media_library.py +381 -0
- bladeworks/preview/media_reporting.py +55 -0
- bladeworks/preview/media_routes.py +155 -0
- bladeworks/preview/media_visuals.py +225 -0
- bladeworks/preview/producer.py +211 -0
- bladeworks/preview/provider.py +74 -0
- bladeworks/preview/rawframe.py +218 -0
- bladeworks/preview/render_jobs.py +311 -0
- bladeworks/preview/routes.py +375 -0
- bladeworks/preview/runner.py +349 -0
- bladeworks/preview/security.py +61 -0
- bladeworks/preview/service.py +234 -0
- bladeworks/preview/session.py +457 -0
- bladeworks/preview/source.py +603 -0
- bladeworks/preview/source_routes.py +151 -0
- bladeworks/preview/studio.py +77 -0
- bladeworks/preview/webrtc.py +295 -0
- bladeworks/spatial_luts/manifest.json +17 -0
- bladeworks/spatial_luts/rec2020_hlg_to_rec709_sdr_v1.cube +4917 -0
- bladeworks/spatial_luts/rec2020_pq_to_rec709_sdr_v1.cube +4917 -0
- bladeworks/studio_static/__init__.py +1 -0
- bladeworks/studio_static/app.js +6519 -0
- bladeworks/studio_static/build-id.js +1 -0
- bladeworks/studio_static/capability-ui.js +68 -0
- bladeworks/studio_static/clip-state.js +335 -0
- bladeworks/studio_static/fcpxml.js +3313 -0
- bladeworks/studio_static/fixtures.js +393 -0
- bladeworks/studio_static/history.js +50 -0
- bladeworks/studio_static/index.html +24 -0
- bladeworks/studio_static/magnetic-timeline.js +1194 -0
- bladeworks/studio_static/mask-ui.js +189 -0
- bladeworks/studio_static/media-visuals.js +224 -0
- bladeworks/studio_static/mock-capabilities.js +409 -0
- bladeworks/studio_static/retime-ui.js +151 -0
- bladeworks/studio_static/runtime.js +1652 -0
- bladeworks/studio_static/scope-ui.js +150 -0
- bladeworks/studio_static/styles.css +1558 -0
- bladeworks/studio_static/templates/catalog.js +77 -0
- bladeworks/studio_static/templates/icons.js +84 -0
- bladeworks/studio_static/templates/inspector.js +553 -0
- bladeworks/studio_static/templates/library.js +61 -0
- bladeworks/studio_static/templates/media.js +185 -0
- bladeworks/studio_static/templates/shared.js +43 -0
- bladeworks/studio_static/templates/shell.js +94 -0
- bladeworks/studio_static/templates/timeline.js +377 -0
- bladeworks/studio_static/templates/topbar.js +83 -0
- bladeworks/studio_static/templates/types.js +7 -0
- bladeworks/studio_static/templates/viewer.js +174 -0
- bladeworks/studio_static/templates.js +31 -0
- bladeworks/studio_static/types.js +18 -0
- bladeworks/studio_static/ui.js +97 -0
- bladeworks/studio_static/viewer-geometry.js +55 -0
- bladeworks/tensor/__init__.py +131 -0
- bladeworks/tensor/audio_delivery.py +280 -0
- bladeworks/tensor/audio_pyav.py +360 -0
- bladeworks/tensor/blend.py +217 -0
- bladeworks/tensor/color.py +53 -0
- bladeworks/tensor/composite.py +33 -0
- bladeworks/tensor/decode.py +1309 -0
- bladeworks/tensor/decode_policy.py +363 -0
- bladeworks/tensor/effects.py +322 -0
- bladeworks/tensor/encode.py +614 -0
- bladeworks/tensor/errors.py +32 -0
- bladeworks/tensor/expr.py +775 -0
- bladeworks/tensor/fx_basic.py +946 -0
- bladeworks/tensor/fx_branched.py +640 -0
- bladeworks/tensor/fx_cohort.py +404 -0
- bladeworks/tensor/fx_color.py +851 -0
- bladeworks/tensor/fx_keyer.py +140 -0
- bladeworks/tensor/fx_mask.py +352 -0
- bladeworks/tensor/fx_warp.py +337 -0
- bladeworks/tensor/hdr.py +122 -0
- bladeworks/tensor/pipeline.py +401 -0
- bladeworks/tensor/plan.py +2231 -0
- bladeworks/tensor/renderer.py +1199 -0
- bladeworks/tensor/requirements.txt +28 -0
- bladeworks/tensor/resolution.py +246 -0
- bladeworks/tensor/sampler.py +598 -0
- bladeworks/tensor/support.py +169 -0
- bladeworks/tensor/swscale_fixedpoint.py +181 -0
- bladeworks/tensor/tr_equirect.py +420 -0
- bladeworks/tensor/tr_handlers.py +443 -0
- bladeworks/tensor/tr_phase5.py +697 -0
- bladeworks/tensor/transitions.py +424 -0
- bladeworks/transitions/__init__.py +10 -0
- bladeworks/transitions/carryovers.py +299 -0
- bladeworks/transitions/cohort.py +981 -0
- bladeworks/transitions/contract.py +490 -0
- bladeworks/transitions/equirectangular.py +915 -0
- bladeworks/transitions/light_deco.py +141 -0
- bladeworks/transitions/panel_motion.py +495 -0
- bladeworks/transitions/stock.py +270 -0
- bladeworks-0.1.0.dist-info/METADATA +66 -0
- bladeworks-0.1.0.dist-info/RECORD +178 -0
- bladeworks-0.1.0.dist-info/WHEEL +5 -0
- bladeworks-0.1.0.dist-info/entry_points.txt +2 -0
- bladeworks-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""F2 transition port: the eight 360° equirectangular transitions (handler ``equirectangular``).
|
|
2
|
+
|
|
3
|
+
Architecture map
|
|
4
|
+
================
|
|
5
|
+
|
|
6
|
+
plan.build_tensor_plan -> transitions.lower_transition (handler "equirectangular")
|
|
7
|
+
-> _lower_equirectangular(item, LowerContext)
|
|
8
|
+
transitions.equirectangular.build_equirectangular_transition_plan(xfade_id,
|
|
9
|
+
parameter_values) -- the SAME registry resolution the CPU builder
|
|
10
|
+
runs (``ffmpeg._build_stock_transition_groups``),
|
|
11
|
+
parameter-aware (direction / speed menu /
|
|
12
|
+
soften edges / border / slices / spacing)
|
|
13
|
+
prefilter None + admitted id -> kind "xfade_custom" (transitions.XfadePayload:
|
|
14
|
+
the wrap-aware ``mod``-longitude expression is
|
|
15
|
+
evaluated by tensor/expr.py exactly like the
|
|
16
|
+
flat registry expressions)
|
|
17
|
+
prefilter equirectangular_gaussian
|
|
18
|
+
-> kind "equirect_gaussian_blur" (this module:
|
|
19
|
+
the stock ``gblur`` graph, see below)
|
|
20
|
+
prefilter equirectangular_bloom
|
|
21
|
+
-> loud reject (see below)
|
|
22
|
+
|
|
23
|
+
Six ids are pure ``xfade=custom`` expressions in the reference and lower to ``xfade_custom``:
|
|
24
|
+
``equirect_circle_wipe``, ``equirect_divide``, ``equirect_push``, ``equirect_reveal_wipe``,
|
|
25
|
+
``equirect_slide``, ``equirect_wipe`` (``ADMITTED_EQUIRECT_IDS``; goldens in
|
|
26
|
+
``experimental_tests/core/test_tensor_transition_golden.py`` -- default parameter values plus
|
|
27
|
+
direction / speed+soften / border / slices variants, float64 CPU bit-exact -- and an
|
|
28
|
+
end-to-end SSIM render in ``test_tensor_transition_golden_f1f2.py``).
|
|
29
|
+
|
|
30
|
+
The two blur variants are NOT expressions in the reference: ``equirect_gaussian_blur`` and
|
|
31
|
+
``equirect_bloom_default`` resolve to ``prefilter`` in {``equirectangular_gaussian``,
|
|
32
|
+
``equirectangular_bloom``} and the CPU builder runs ``ffmpeg._build_equirectangular_blur_group``
|
|
33
|
+
(the plan's ``expression`` field for those ids is the dead five-tap expression the builder
|
|
34
|
+
never emits, so evaluating it would be a silent approximation).
|
|
35
|
+
|
|
36
|
+
360° Gaussian Blur (ported, kind ``equirect_gaussian_blur``) -- per side, in the 8-bit
|
|
37
|
+
straight ``gbrap`` code domain of the reference sides (``color.premultiplied_to_code``):
|
|
38
|
+
|
|
39
|
+
clean = the side
|
|
40
|
+
strip = [side | side | side] (``split`` + ``hstack=inputs=3``)
|
|
41
|
+
strong = gblur(strip, sigma, sigmaV, steps=3)[centre crop]
|
|
42
|
+
mild = gblur(strip, 0.28 sigma, 0.28 sigmaV, steps=2)[centre crop]
|
|
43
|
+
shoulder = trunc(clean*(1-sw) + mild*sw) (``blend=all_expr``, ``sw = min(1, 2*activity)``)
|
|
44
|
+
side_out = trunc(shoulder*(1-activity) + strong*activity)
|
|
45
|
+
activity = transitions.equirectangular.equirectangular_blur_activity("gaussian_skew", duration) at T = k*tb
|
|
46
|
+
|
|
47
|
+
then the two blurred sides go through the small ownership ``xfade=custom`` expression
|
|
48
|
+
(``gaussian_lead``, ``ffmpeg._build_equirectangular_blur_group``) evaluated by
|
|
49
|
+
``expr.xfade_custom_rgba`` like every other xfade. ``gblur`` is FFmpeg's Alvarez-Mazorra
|
|
50
|
+
recursive Gaussian (``vf_gblur.c``): per axis ``steps`` passes of a causal then anti-causal
|
|
51
|
+
first-order IIR ``y[n] = x[n] + nu*y[n-1]`` whose boundary scale ``1/(1-nu)`` is exactly an
|
|
52
|
+
infinite replicate pad, ``postscale = (nu/lambda)^steps`` per axis, float32 buffers, clip to
|
|
53
|
+
[0, 255], ``lrintf`` (round half even). Here each IIR pass is the same recursion evaluated
|
|
54
|
+
in blocks (lower-Toeplitz matmul + carry, ``_iir_pass_blocked``) on the triple strip -- so the
|
|
55
|
+
longitude seam wraps exactly like the reference's ``hstack`` and latitude clamps at the poles. The envelope,
|
|
56
|
+
sigma texts (``ffmpeg._number`` -> float32 AVOption) and the ownership / activity expression
|
|
57
|
+
strings are taken from ``ffmpeg.py`` itself, never
|
|
58
|
+
re-typed, so the port cannot drift from the emitted graph. Golden:
|
|
59
|
+
``test_tensor_transition_golden_f1f2.py`` (gblur vs the ``ffmpeg`` CLI on plates, the whole
|
|
60
|
+
side graph vs the ``ffmpeg`` CLI, and the e2e SSIM render vs the CPU reference).
|
|
61
|
+
|
|
62
|
+
360° Bloom stays a loud reject (``support.reject("transition (other)", ...)``): its side graph
|
|
63
|
+
adds ``colorlevels`` + ``eq`` (a YUV round trip through swscale) + a ``curves`` natural
|
|
64
|
+
spline luminance mask + ``alphamerge`` + two alpha ``fade`` ramps + two ``overlay`` passes;
|
|
65
|
+
six stock filters with their own conversions and rounding, each needing its own golden.
|
|
66
|
+
|
|
67
|
+
Contract: see ``tensor/transitions.py`` (``Lowered`` / ``XfadePayload`` / apply ports).
|
|
68
|
+
|
|
69
|
+
Main callers:
|
|
70
|
+
- ``transitions.lower_transition`` (via ``register_handler``); ``transitions.apply_transition``
|
|
71
|
+
(via ``register`` of the ``EquirectGaussian`` transition).
|
|
72
|
+
- ``experimental_tests/core/test_tensor_transition_golden.py`` (``resolve_equirectangular_expression``),
|
|
73
|
+
``test_tensor_transition_golden_f1f2.py`` (``gblur_gbrap``, ``gaussian_blur_side``).
|
|
74
|
+
|
|
75
|
+
Why this exists: the 360° ids are a separate registry (``transitions.equirectangular``) with
|
|
76
|
+
its own parameter contract and its own plan type; keeping the lowering here (one owner per
|
|
77
|
+
file) leaves ``tensor/transitions.py``'s xfade path untouched while sharing its apply kernel.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
from __future__ import annotations
|
|
81
|
+
|
|
82
|
+
import math
|
|
83
|
+
from dataclasses import dataclass
|
|
84
|
+
from fractions import Fraction
|
|
85
|
+
from typing import Any, Final, Mapping, Optional
|
|
86
|
+
|
|
87
|
+
import numpy as np
|
|
88
|
+
import torch
|
|
89
|
+
|
|
90
|
+
from ..core.filter_text import format_number as _number, format_seconds as _seconds
|
|
91
|
+
from ..transitions.equirectangular import equirectangular_blur_activity
|
|
92
|
+
from ..core.model import RenderTransition
|
|
93
|
+
from ..transitions.equirectangular import build_equirectangular_transition_plan
|
|
94
|
+
from .color import code_to_premultiplied, premultiplied_to_code
|
|
95
|
+
from .errors import TensorRenderUnsupported
|
|
96
|
+
from .expr import Environment, evaluate, parse, quantize_uint8, xfade_custom_rgba, xfade_progress
|
|
97
|
+
from .support import reject
|
|
98
|
+
from .transitions import (
|
|
99
|
+
ApplyContext,
|
|
100
|
+
LowerContext,
|
|
101
|
+
Lowered,
|
|
102
|
+
Transition,
|
|
103
|
+
XfadePayload,
|
|
104
|
+
register,
|
|
105
|
+
register_handler,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# 360° ids the plan admits (append-only; every entry has a golden -- see module doc).
|
|
109
|
+
ADMITTED_EQUIRECT_IDS: Final[tuple[str, ...]] = (
|
|
110
|
+
"equirect_circle_wipe",
|
|
111
|
+
"equirect_divide",
|
|
112
|
+
"equirect_push",
|
|
113
|
+
"equirect_reveal_wipe",
|
|
114
|
+
"equirect_slide",
|
|
115
|
+
"equirect_wipe",
|
|
116
|
+
"equirect_gaussian_blur",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# The reference's ``ffmpeg._build_equirectangular_blur_group`` shape for the Gaussian id
|
|
120
|
+
# (asserted at lowering so a registry change is loud, not silently re-interpreted).
|
|
121
|
+
_GAUSSIAN_ACTIVITY_PROFILE: Final = "gaussian_skew"
|
|
122
|
+
_GAUSSIAN_OWNERSHIP_PROFILE: Final = "gaussian_lead"
|
|
123
|
+
_STRONG_STEPS: Final = 3
|
|
124
|
+
_MILD_STEPS: Final = 2
|
|
125
|
+
_MILD_SIGMA_SCALE: Final = 0.28
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def resolve_equirectangular_expression(xfade_id: str, parameter_values: Optional[Mapping[str, Any]] = None) -> str:
|
|
129
|
+
"""The reference's resolved 360° ``xfade=custom`` expression for an admitted expression id, loudly.
|
|
130
|
+
|
|
131
|
+
Runs the very registry resolution the CPU builder runs
|
|
132
|
+
(``build_equirectangular_transition_plan(xfade_id, parameter_values)``); refuses ids
|
|
133
|
+
outside ``ADMITTED_EQUIRECT_IDS`` and plans that carry a blur prefilter (their
|
|
134
|
+
``expression`` is dead text, see module doc -- the Gaussian id lowers to its own kind).
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
if xfade_id not in ADMITTED_EQUIRECT_IDS:
|
|
138
|
+
raise TensorRenderUnsupported(
|
|
139
|
+
f"360° transition {xfade_id!r} is not ported to the tensor renderer "
|
|
140
|
+
f"(admitted: {', '.join(ADMITTED_EQUIRECT_IDS)})"
|
|
141
|
+
)
|
|
142
|
+
plan = build_equirectangular_transition_plan(xfade_id, dict(parameter_values or {}))
|
|
143
|
+
if plan.mode != "custom" or plan.prefilter is not None:
|
|
144
|
+
raise TensorRenderUnsupported(
|
|
145
|
+
f"360° transition {xfade_id!r} resolves to mode={plan.mode!r} prefilter={plan.prefilter!r}, "
|
|
146
|
+
"not a pure custom expression (the Gaussian blur is its own kind, see tr_equirect)"
|
|
147
|
+
)
|
|
148
|
+
return plan.expression
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# 360° Gaussian Blur: the stock gblur side graph
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(frozen=True)
|
|
157
|
+
class EquirectGaussianPayload:
|
|
158
|
+
xfade_id: str
|
|
159
|
+
horizontal_strength: float # plan.strength: sigma = max(1e-4, min(W, H) * strength)
|
|
160
|
+
vertical_strength: float # plan.spread
|
|
161
|
+
duration_text: str # ffmpeg._seconds(owned window) as the graph writes it
|
|
162
|
+
ownership_expression: str # the small ownership xfade text (evaluated by expr)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def gaussian_ownership_expression() -> str:
|
|
166
|
+
"""The ``gaussian_lead`` ownership ``xfade=custom`` text of ``ffmpeg._build_equirectangular_blur_group``.
|
|
167
|
+
|
|
168
|
+
Copied character-for-character from the builder (it is an inline literal there);
|
|
169
|
+
``test_tensor_transition_golden_f1f2.py`` asserts the emitted filter script carries it.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
q = "(1-P)"
|
|
173
|
+
normalized = f"clip((({q})-0.02)/0.825,0,1)"
|
|
174
|
+
weight = f"(({normalized})*({normalized})*(3-2*({normalized})))"
|
|
175
|
+
return f"if(gte(P,1),A,if(lte(P,0),B,A*(1-({weight}))+B*({weight})))"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def gblur_params(sigma: float, steps: int) -> tuple[float, float]:
|
|
179
|
+
"""``vf_gblur.c`` ``set_params``: ``(nu, postscale)`` for one axis, in the C precisions.
|
|
180
|
+
|
|
181
|
+
``sigma`` is the value the AVOption parsed (float32 of the ``_number`` text); ``lambda``
|
|
182
|
+
and ``dnu`` are doubles, ``nu`` is stored as float32, ``postscale = pow(dnu/lambda, steps)``.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
sigma32 = np.float32(sigma)
|
|
186
|
+
lam = float(sigma32 * sigma32) / (2.0 * steps) # C: float * float, then promoted
|
|
187
|
+
dnu = (1.0 + 2.0 * lam - math.sqrt(1.0 + 4.0 * lam)) / (2.0 * lam)
|
|
188
|
+
postscale = float(np.float32(math.pow(dnu / lam, steps)))
|
|
189
|
+
nu = float(np.float32(dnu))
|
|
190
|
+
if not math.isfinite(postscale) or postscale == 0.0:
|
|
191
|
+
postscale = 1.0
|
|
192
|
+
if not math.isfinite(nu):
|
|
193
|
+
nu = 0.0
|
|
194
|
+
return nu, postscale
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
_IIR_BLOCK: Final = 128
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _iir_pass_blocked(rows: torch.Tensor, nu: float, *, causal: bool) -> torch.Tensor:
|
|
201
|
+
"""One first-order IIR pass ``y[n] = x[n] + nu*y[n-1]`` along the last axis with the C boundary
|
|
202
|
+
(``ptr[0] *= 1/(1-nu)``, i.e. ``y[-1] = x[0]/(1-nu)``), evaluated in blocks of ``_IIR_BLOCK``:
|
|
203
|
+
|
|
204
|
+
y_block = x_block @ T + carry * nu^(1..B) T[j, i] = nu^(i-j) for i >= j (lower Toeplitz)
|
|
205
|
+
carry = y_block[-1]
|
|
206
|
+
|
|
207
|
+
-- the exact recursion, one matmul + one outer product per block instead of ``L`` scalar
|
|
208
|
+
steps (fast on MPS and CPU) and no unfolded convolution buffers (``conv1d`` on the CPU
|
|
209
|
+
unfolds ``rows x taps x L`` floats: gigabytes at 1080p). ``causal=False`` mirrors the axis
|
|
210
|
+
(``ptr[W-1] *= bscale; ptr[x-1] += nu*ptr[x]``).
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
if not causal:
|
|
214
|
+
return _iir_pass_blocked(rows.flip(-1), nu, causal=True).flip(-1)
|
|
215
|
+
length = rows.shape[-1]
|
|
216
|
+
block = min(_IIR_BLOCK, length)
|
|
217
|
+
device, dtype = rows.device, rows.dtype
|
|
218
|
+
powers = torch.pow(torch.full((block,), nu, device=device, dtype=dtype), torch.arange(1, block + 1, device=device, dtype=dtype))
|
|
219
|
+
index = torch.arange(block, device=device)
|
|
220
|
+
exponent = (index.view(1, block) - index.view(block, 1)).to(dtype) # i - j
|
|
221
|
+
toeplitz = torch.where(exponent >= 0, torch.pow(torch.full_like(exponent, nu), exponent.clamp(min=0)), torch.zeros_like(exponent))
|
|
222
|
+
carry = rows[:, :1] * float(np.float32(1.0 / (1.0 - nu))) # y[-1] = x[0] * bscale
|
|
223
|
+
pieces = []
|
|
224
|
+
for start in range(0, length, block):
|
|
225
|
+
chunk = rows[:, start:start + block]
|
|
226
|
+
width = chunk.shape[-1]
|
|
227
|
+
y = chunk @ toeplitz[:width, :width] + carry * powers[:width].view(1, width)
|
|
228
|
+
carry = y[:, -1:]
|
|
229
|
+
pieces.append(y)
|
|
230
|
+
return torch.cat(pieces, dim=-1)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _iir_pass_recursion(rows: torch.Tensor, nu: float, *, causal: bool) -> torch.Tensor:
|
|
234
|
+
"""The C recursion itself (``horiz_slice_c``), vectorized across rows, sequential along the
|
|
235
|
+
axis: ``ptr[0] *= bscale; ptr[x] += nu*ptr[x-1]`` (and mirrored). Reference implementation
|
|
236
|
+
for the goldens (same float32 evaluation order as ffmpeg); ``L`` vector ops per pass."""
|
|
237
|
+
|
|
238
|
+
out = rows.clone()
|
|
239
|
+
length = out.shape[-1]
|
|
240
|
+
bscale = float(np.float32(1.0 / (1.0 - nu)))
|
|
241
|
+
columns = out.unbind(-1) # views into ``out``; in-place updates keep the recursion
|
|
242
|
+
if causal:
|
|
243
|
+
columns[0].mul_(bscale)
|
|
244
|
+
for x in range(1, length):
|
|
245
|
+
columns[x].add_(columns[x - 1], alpha=nu)
|
|
246
|
+
else:
|
|
247
|
+
columns[length - 1].mul_(bscale)
|
|
248
|
+
for x in range(length - 1, 0, -1):
|
|
249
|
+
columns[x - 1].add_(columns[x], alpha=nu)
|
|
250
|
+
return out
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _iir_axis(planes: torch.Tensor, nu: float, steps: int, method: str = "blocked") -> torch.Tensor:
|
|
254
|
+
"""``steps`` passes of causal + anti-causal IIR along the last axis (``horiz_slice_c``).
|
|
255
|
+
|
|
256
|
+
``method``: ``"blocked"`` (the runtime, any device) or ``"recursion"`` (the C loop, goldens);
|
|
257
|
+
both are goldened against ``ffmpeg gblur`` in ``test_tensor_transition_golden_f1f2.py``.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
if nu <= 0.0:
|
|
261
|
+
return planes
|
|
262
|
+
one_pass = {"blocked": _iir_pass_blocked, "recursion": _iir_pass_recursion}[method]
|
|
263
|
+
rows = planes.reshape(-1, planes.shape[-1])
|
|
264
|
+
for _ in range(steps):
|
|
265
|
+
rows = one_pass(rows, nu, causal=True)
|
|
266
|
+
rows = one_pass(rows, nu, causal=False)
|
|
267
|
+
return rows.reshape(planes.shape)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def gblur_gbrap(
|
|
271
|
+
code: torch.Tensor, *, sigma: float, sigma_v: float, steps: int,
|
|
272
|
+
crop_x: Optional[tuple[int, int]] = None, method: str = "blocked",
|
|
273
|
+
) -> torch.Tensor:
|
|
274
|
+
"""``gblur=sigma=..:sigmaV=..:steps=..`` on an 8-bit code frame ``[planes, H, W]`` (all planes).
|
|
275
|
+
|
|
276
|
+
Input values are code values (any float dtype); output is the C result: float32 IIR passes
|
|
277
|
+
horizontally then vertically, ``* postscale_h * postscale_v``, clip to [0, 255], ``lrintf``.
|
|
278
|
+
``sigma`` / ``sigma_v`` are the AVOption values (callers pass the ``_number`` text's float).
|
|
279
|
+
``crop_x = (x0, x1)`` returns columns ``x0:x1`` (the reference's centre ``crop`` after the
|
|
280
|
+
triple-strip blur); columns are independent in the vertical pass, so it is applied first.
|
|
281
|
+
``method`` selects the IIR implementation (see ``_iir_axis``).
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
nu_h, post_h = gblur_params(sigma, steps)
|
|
285
|
+
nu_v, post_v = gblur_params(sigma_v, steps)
|
|
286
|
+
work = code.to(torch.float32)
|
|
287
|
+
work = _iir_axis(work, nu_h, steps, method)
|
|
288
|
+
if crop_x is not None:
|
|
289
|
+
work = work[..., crop_x[0]:crop_x[1]]
|
|
290
|
+
work = _iir_axis(work.transpose(-1, -2).contiguous(), nu_v, steps, method).transpose(-1, -2)
|
|
291
|
+
work = (work * float(np.float32(post_h) * np.float32(post_v))).clamp(0.0, 255.0)
|
|
292
|
+
return torch.round(work).to(code.dtype)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _sigma_options(width: int, height: int, strength: float) -> tuple[float, float]:
|
|
296
|
+
"""The strong and mild sigmas the graph writes for one axis, as the ``gblur`` AVOption reads them.
|
|
297
|
+
|
|
298
|
+
``ffmpeg._equirectangular_blur_side``: ``sigma = max(0.0001, min(W, H) * strength)`` (double),
|
|
299
|
+
strong text ``_number(sigma)``, mild text ``_number(sigma * 0.28)``; AVOption floats are float32.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
sigma = max(0.0001, min(width, height) * strength)
|
|
303
|
+
return float(np.float32(float(_number(sigma)))), float(np.float32(float(_number(sigma * _MILD_SIGMA_SCALE))))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _scalar(text: str, **variables: float) -> float:
|
|
307
|
+
value = evaluate(parse(text), Environment(variables=dict(variables), samplers={}, device=torch.device("cpu"), dtype=torch.float64))
|
|
308
|
+
if isinstance(value, torch.Tensor):
|
|
309
|
+
raise AssertionError(f"expected a scalar envelope, got a tensor for {text!r}")
|
|
310
|
+
return float(value)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def gaussian_blur_side(
|
|
314
|
+
code: torch.Tensor,
|
|
315
|
+
*,
|
|
316
|
+
width: int,
|
|
317
|
+
height: int,
|
|
318
|
+
horizontal_strength: float,
|
|
319
|
+
vertical_strength: float,
|
|
320
|
+
activity: float,
|
|
321
|
+
) -> torch.Tensor:
|
|
322
|
+
"""One side of ``ffmpeg._equirectangular_blur_side`` (Gaussian branch) on an 8-bit code frame.
|
|
323
|
+
|
|
324
|
+
``activity`` is the envelope value at this frame (``transitions.equirectangular.equirectangular_blur_activity``
|
|
325
|
+
at ``T``); the two ``blend=all_expr`` stages truncate like the C ``uint8`` store.
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
strong_h, mild_h = _sigma_options(width, height, horizontal_strength)
|
|
329
|
+
strong_v, mild_v = _sigma_options(width, height, vertical_strength)
|
|
330
|
+
strip = torch.cat((code, code, code), dim=-1)
|
|
331
|
+
centre = (width, 2 * width)
|
|
332
|
+
strong = gblur_gbrap(strip, sigma=strong_h, sigma_v=strong_v, steps=_STRONG_STEPS, crop_x=centre)
|
|
333
|
+
mild = gblur_gbrap(strip, sigma=mild_h, sigma_v=mild_v, steps=_MILD_STEPS, crop_x=centre)
|
|
334
|
+
shoulder_weight = min(1.0, 2.0 * activity)
|
|
335
|
+
shoulder = quantize_uint8(code * (1.0 - shoulder_weight) + mild * shoulder_weight)
|
|
336
|
+
return quantize_uint8(shoulder * (1.0 - activity) + strong * activity)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def blur_activity(duration_text: str, *, frame_index: int, frame_duration: Fraction) -> float:
|
|
340
|
+
"""``transitions.equirectangular.equirectangular_blur_activity("gaussian_skew", duration)`` at the k-th frame:
|
|
341
|
+
``T = pts * av_q2d(tb)`` with ``pts = k`` and ``tb`` the frame duration (``settb=expr=1/fps``)."""
|
|
342
|
+
|
|
343
|
+
time_seconds = frame_index * (frame_duration.numerator / frame_duration.denominator)
|
|
344
|
+
return _scalar(equirectangular_blur_activity(_GAUSSIAN_ACTIVITY_PROFILE, duration_text), T=time_seconds)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class EquirectGaussian(Transition):
|
|
348
|
+
kind = "equirect_gaussian_blur"
|
|
349
|
+
|
|
350
|
+
def apply(self, payload: EquirectGaussianPayload, a: torch.Tensor, b: torch.Tensor, ctx: ApplyContext) -> torch.Tensor:
|
|
351
|
+
progress = xfade_progress(ctx.frame_index, ctx.frame_count)
|
|
352
|
+
if progress >= 1.0:
|
|
353
|
+
return a
|
|
354
|
+
activity = blur_activity(payload.duration_text, frame_index=ctx.frame_index, frame_duration=ctx.frame_duration)
|
|
355
|
+
sides = []
|
|
356
|
+
for side in (a, b):
|
|
357
|
+
sides.append(gaussian_blur_side(
|
|
358
|
+
premultiplied_to_code(side), width=ctx.width, height=ctx.height,
|
|
359
|
+
horizontal_strength=payload.horizontal_strength, vertical_strength=payload.vertical_strength,
|
|
360
|
+
activity=activity,
|
|
361
|
+
))
|
|
362
|
+
out_code = xfade_custom_rgba(parse(payload.ownership_expression), sides[0], sides[1], progress=progress)
|
|
363
|
+
return code_to_premultiplied(out_code)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
register(EquirectGaussian())
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ---------------------------------------------------------------------------
|
|
370
|
+
# lowering
|
|
371
|
+
# ---------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _lower_equirectangular(item: RenderTransition, ctx: LowerContext) -> Lowered:
|
|
375
|
+
if item.xfade_id is None:
|
|
376
|
+
raise reject("transition (other)", f"{item.path}: equirectangular transition without a registry id")
|
|
377
|
+
plan = build_equirectangular_transition_plan(item.xfade_id, dict(item.parameter_values or {}))
|
|
378
|
+
if plan.prefilter == "equirectangular_bloom":
|
|
379
|
+
raise reject(
|
|
380
|
+
"transition (other)",
|
|
381
|
+
f"{item.path}: transition {item.name!r} xfade_id={item.xfade_id!r} resolves to the stock "
|
|
382
|
+
"bloom graph (ffmpeg._build_equirectangular_blur_group: hstack-wrapped gblur + colorlevels + eq "
|
|
383
|
+
"+ curves luminance mask + alphamerge + alpha fades + overlays), not an expression; F2 bloom port pending",
|
|
384
|
+
)
|
|
385
|
+
if plan.prefilter == "equirectangular_gaussian":
|
|
386
|
+
if item.xfade_id not in ADMITTED_EQUIRECT_IDS:
|
|
387
|
+
raise reject("transition (other)", f"{item.path}: {item.xfade_id!r} gaussian graph is not admitted")
|
|
388
|
+
if (plan.activity_profile, plan.ownership_profile) != (_GAUSSIAN_ACTIVITY_PROFILE, _GAUSSIAN_OWNERSHIP_PROFILE):
|
|
389
|
+
raise reject(
|
|
390
|
+
"transition (other)",
|
|
391
|
+
f"{item.path}: {item.xfade_id!r} resolves to profiles ({plan.activity_profile!r}, "
|
|
392
|
+
f"{plan.ownership_profile!r}); the tensor port covers ({_GAUSSIAN_ACTIVITY_PROFILE!r}, "
|
|
393
|
+
f"{_GAUSSIAN_OWNERSHIP_PROFILE!r}) only",
|
|
394
|
+
)
|
|
395
|
+
return Lowered(
|
|
396
|
+
kind="equirect_gaussian_blur",
|
|
397
|
+
xfade_id=item.xfade_id,
|
|
398
|
+
payload=EquirectGaussianPayload(
|
|
399
|
+
xfade_id=item.xfade_id,
|
|
400
|
+
horizontal_strength=plan.strength,
|
|
401
|
+
vertical_strength=plan.spread,
|
|
402
|
+
duration_text=_seconds(ctx.frame_count * ctx.frame_duration),
|
|
403
|
+
ownership_expression=gaussian_ownership_expression(),
|
|
404
|
+
),
|
|
405
|
+
)
|
|
406
|
+
if plan.prefilter is not None:
|
|
407
|
+
raise reject(
|
|
408
|
+
"transition (other)",
|
|
409
|
+
f"{item.path}: transition {item.name!r} xfade_id={item.xfade_id!r} resolves to prefilter={plan.prefilter!r}",
|
|
410
|
+
)
|
|
411
|
+
if item.xfade_id not in ADMITTED_EQUIRECT_IDS:
|
|
412
|
+
raise reject(
|
|
413
|
+
"transition (other)",
|
|
414
|
+
f"{item.path}: transition {item.name!r} xfade_id={item.xfade_id!r} is a 360° custom "
|
|
415
|
+
"expression but is not in tr_equirect.ADMITTED_EQUIRECT_IDS (needs its golden)",
|
|
416
|
+
)
|
|
417
|
+
return Lowered(kind="xfade_custom", xfade_id=item.xfade_id, payload=XfadePayload(item.xfade_id, plan.expression))
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
register_handler("equirectangular", _lower_equirectangular)
|