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,598 @@
|
|
|
1
|
+
"""One homography per layer: crop / conform / corner pin / transform -> ``grid_sample``.
|
|
2
|
+
|
|
3
|
+
Architecture map
|
|
4
|
+
================
|
|
5
|
+
|
|
6
|
+
GeometryPlan.snapshot(t) (geometry.py -- the exact semantics)
|
|
7
|
+
composed_quad, camera_placement, crop_rect, source_rect
|
|
8
|
+
-> conform_matrix : source raster (edge coords) -> clip canvas
|
|
9
|
+
-> composed matrix : clip canvas -> clip canvas (corner pin, then affine)
|
|
10
|
+
-> canvas_to_project : clip canvas -> project (inert group placement)
|
|
11
|
+
-> layer_homography : the product, source edge coords -> project edge coords
|
|
12
|
+
source_alpha_window : integer pixel-index window (trim / crop / pan / conform none)
|
|
13
|
+
resize_exact_aspect_opaque(...) : encoded-domain legacy-swscale bilinear minification
|
|
14
|
+
for the calibrated whole-raster Fit / Fill fast path
|
|
15
|
+
warp(source_rgba, H, out_size) : inverse-map every output pixel centre through H,
|
|
16
|
+
bilinear ``grid_sample`` with transparent (zeros) padding
|
|
17
|
+
|
|
18
|
+
Conventions (the trap list in ``plan_research/xyzt_inventory.md`` §5)
|
|
19
|
+
--------------------------------------------------------------------
|
|
20
|
+
* Everything here is in *edge* coordinates: a raster of ``W`` pixels spans
|
|
21
|
+
``[0, W]`` and pixel ``i`` is centred at ``i + 0.5``. ``geometry.py`` quads
|
|
22
|
+
are edge coordinates too (``compose_spatial_quad`` maps the project rectangle
|
|
23
|
+
``(0,0)-(W,H)``). FFmpeg's ``perspective`` works on pixel *indices*; the
|
|
24
|
+
reference converts with ``correct_quad_for_pixel_centers`` -- applying the
|
|
25
|
+
edge homography to output pixel centres ``(x+0.5, y+0.5)`` and sampling with
|
|
26
|
+
``align_corners=False`` is that same correction, so no half-pixel shift is
|
|
27
|
+
applied here. (Proof: for a pure scale ``s``, edge ``x = x0 + s*u`` at
|
|
28
|
+
centres gives index ``x_i = x0 + s*u_i + (s-1)/2`` -- exactly the reference's
|
|
29
|
+
``(scale-1)/2`` conform term.)
|
|
30
|
+
* Rotation sign, +Y-up flip, project-height units, and the anchor rule are
|
|
31
|
+
*not* re-derived here: ``composed_quad`` already contains them.
|
|
32
|
+
* Crop edges are percent of *source* height and arrive resolved in
|
|
33
|
+
``source_rect``/``crop_rect``; the alpha window uses the reference's
|
|
34
|
+
inclusive integer-index rule (``between(X, left, W-right)``).
|
|
35
|
+
* Sampling is bilinear in calibrated linear light on *straight* RGBA with a
|
|
36
|
+
transparent border, like the reference's ``lutrgb pow(1.94) -> perspective``;
|
|
37
|
+
callers premultiply *after* the warp.
|
|
38
|
+
|
|
39
|
+
Main callers:
|
|
40
|
+
- ``renderer.render_document`` (per layer, per frame).
|
|
41
|
+
- ``experimental_tests/core/test_tensor_sampler.py`` (corner-error test).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import math
|
|
47
|
+
from functools import lru_cache
|
|
48
|
+
from typing import Optional
|
|
49
|
+
|
|
50
|
+
import numpy as np
|
|
51
|
+
import torch
|
|
52
|
+
import torch.nn.functional as F
|
|
53
|
+
|
|
54
|
+
from ..core.geometry import FrameGeometry, GeometrySnapshot, Quad
|
|
55
|
+
from .swscale_fixedpoint import c_div as _c_div, finalize_swscale_filter
|
|
56
|
+
|
|
57
|
+
Matrix = np.ndarray # 3x3 float64 homography, row vector convention h @ [x, y, 1]^T
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# --------------------------------------------------------------------------- matrices
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def identity_matrix() -> Matrix:
|
|
64
|
+
return np.eye(3, dtype=np.float64)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def affine_matrix(scale_x: float, scale_y: float, translate_x: float, translate_y: float) -> Matrix:
|
|
68
|
+
"""``x' = scale_x * x + translate_x`` (and likewise for y)."""
|
|
69
|
+
|
|
70
|
+
return np.array(
|
|
71
|
+
[[scale_x, 0.0, translate_x], [0.0, scale_y, translate_y], [0.0, 0.0, 1.0]],
|
|
72
|
+
dtype=np.float64,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def homography_from_points(source: Quad, destination: Quad) -> Matrix:
|
|
77
|
+
"""Return the 3x3 homography mapping the four ``source`` points onto ``destination``.
|
|
78
|
+
|
|
79
|
+
Quad order is (top-left, top-right, bottom-left, bottom-right) as in
|
|
80
|
+
``geometry.py``. Four exact correspondences pin a projective map; the
|
|
81
|
+
standard direct linear transform with ``h33 = 1`` is solved with numpy.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
rows: list[list[float]] = []
|
|
85
|
+
rhs: list[float] = []
|
|
86
|
+
for (x, y), (u, v) in zip(source, destination):
|
|
87
|
+
rows.append([x, y, 1.0, 0.0, 0.0, 0.0, -u * x, -u * y])
|
|
88
|
+
rhs.append(u)
|
|
89
|
+
rows.append([0.0, 0.0, 0.0, x, y, 1.0, -v * x, -v * y])
|
|
90
|
+
rhs.append(v)
|
|
91
|
+
solution = np.linalg.solve(np.array(rows, dtype=np.float64), np.array(rhs, dtype=np.float64))
|
|
92
|
+
return np.array(
|
|
93
|
+
[[solution[0], solution[1], solution[2]],
|
|
94
|
+
[solution[3], solution[4], solution[5]],
|
|
95
|
+
[solution[6], solution[7], 1.0]],
|
|
96
|
+
dtype=np.float64,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def apply_matrix(matrix: Matrix, points: Quad) -> Quad:
|
|
101
|
+
result = []
|
|
102
|
+
for x, y in points:
|
|
103
|
+
hx, hy, hw = matrix @ np.array([x, y, 1.0])
|
|
104
|
+
result.append((float(hx / hw), float(hy / hw)))
|
|
105
|
+
return tuple(result) # type: ignore[return-value]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def is_identity(matrix: Matrix, *, tolerance: float = 1e-9) -> bool:
|
|
109
|
+
return bool(np.allclose(matrix / matrix[2, 2], np.eye(3), atol=tolerance, rtol=0.0))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def project_rect_quad(width: int, height: int) -> Quad:
|
|
113
|
+
return ((0.0, 0.0), (float(width), 0.0), (0.0, float(height)), (float(width), float(height)))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# --------------------------------------------------------------------------- semantics
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def conform_matrix(snapshot: GeometrySnapshot, frame: FrameGeometry, conform: str) -> Matrix:
|
|
120
|
+
"""Source raster (edge coords) -> clip canvas (edge coords) for one frame.
|
|
121
|
+
|
|
122
|
+
Mirrors ``GeometryPlan._conform_filters`` / ``resolve_camera_placement``:
|
|
123
|
+
* Crop and Pan replace conform by the calibrated camera (``exact_scale``,
|
|
124
|
+
``exact_origin``) computed from the full source and the reference window;
|
|
125
|
+
* Fit / Fill: ``scale = min/max(P/S)`` and ``x = (Pw - s*Ws)/2 + s*u``;
|
|
126
|
+
* None: 1:1 pixels, integer-centred like ``crop``/``pad`` (truncation).
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
camera = snapshot.camera_placement
|
|
130
|
+
if camera is not None:
|
|
131
|
+
scale = camera.exact_scale
|
|
132
|
+
return affine_matrix(scale, scale, camera.exact_origin_x, camera.exact_origin_y)
|
|
133
|
+
source_width, source_height = frame.source_width, frame.source_height
|
|
134
|
+
project_width, project_height = frame.project_width, frame.project_height
|
|
135
|
+
if conform == "none":
|
|
136
|
+
return affine_matrix(
|
|
137
|
+
1.0, 1.0,
|
|
138
|
+
float(_truncated_half(project_width - source_width)),
|
|
139
|
+
float(_truncated_half(project_height - source_height)),
|
|
140
|
+
)
|
|
141
|
+
if conform == "fit":
|
|
142
|
+
scale = min(project_width / source_width, project_height / source_height)
|
|
143
|
+
elif conform == "fill":
|
|
144
|
+
scale = max(project_width / source_width, project_height / source_height)
|
|
145
|
+
else:
|
|
146
|
+
raise ValueError(f"unknown conform {conform!r}")
|
|
147
|
+
return affine_matrix(
|
|
148
|
+
scale, scale,
|
|
149
|
+
(project_width - scale * source_width) / 2.0,
|
|
150
|
+
(project_height - scale * source_height) / 2.0,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _truncated_half(delta: int) -> int:
|
|
155
|
+
"""FFmpeg ``crop``/``pad`` evaluate ``(a-b)/2`` as a double and store it in an int."""
|
|
156
|
+
|
|
157
|
+
return int(delta / 2.0)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def composed_matrix(snapshot: GeometrySnapshot, frame: FrameGeometry) -> Matrix:
|
|
161
|
+
"""Clip canvas -> clip canvas: corner pin then anchor transform (``composed_quad``)."""
|
|
162
|
+
|
|
163
|
+
return homography_from_points(
|
|
164
|
+
project_rect_quad(frame.project_width, frame.project_height), snapshot.composed_quad
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def layer_homography(
|
|
169
|
+
snapshot: GeometrySnapshot,
|
|
170
|
+
*,
|
|
171
|
+
frame: FrameGeometry,
|
|
172
|
+
conform: str,
|
|
173
|
+
canvas_to_project: Matrix,
|
|
174
|
+
) -> Matrix:
|
|
175
|
+
"""Source edge coords -> project edge coords: ``canvas_to_project @ composed @ conform``."""
|
|
176
|
+
|
|
177
|
+
return canvas_to_project @ composed_matrix(snapshot, frame) @ conform_matrix(snapshot, frame, conform)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def source_alpha_window(
|
|
181
|
+
snapshot: GeometrySnapshot,
|
|
182
|
+
*,
|
|
183
|
+
frame: FrameGeometry,
|
|
184
|
+
conform: str,
|
|
185
|
+
crop_mode: Optional[str],
|
|
186
|
+
) -> Optional[tuple[int, int, int, int]]:
|
|
187
|
+
"""Return ``(x0, x1, y0, y1)`` pixel-index bounds (half-open) that stay opaque, or None.
|
|
188
|
+
|
|
189
|
+
* Trim: the integer extraction rectangle (``crop`` then transparent ``pad``).
|
|
190
|
+
* Crop / Pan: the reference's ``geq`` window keeps integer sample indices
|
|
191
|
+
with ``left <= X <= W - right`` (inclusive, fractional edges).
|
|
192
|
+
* Conform None: ``crop`` to the project size, centred by truncation.
|
|
193
|
+
Windows intersect; a source without any window returns None.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
source_width, source_height = frame.source_width, frame.source_height
|
|
197
|
+
x0, x1, y0, y1 = 0, source_width, 0, source_height
|
|
198
|
+
if crop_mode == "trim" and snapshot.crop_rect is not None:
|
|
199
|
+
rect = snapshot.crop_rect
|
|
200
|
+
x0, x1 = max(x0, rect.x), min(x1, rect.x + rect.width)
|
|
201
|
+
y0, y1 = max(y0, rect.y), min(y1, rect.y + rect.height)
|
|
202
|
+
elif crop_mode in {"crop", "pan"} and snapshot.source_rect is not None:
|
|
203
|
+
rect = snapshot.source_rect
|
|
204
|
+
x0 = max(x0, math.ceil(rect.x - 1e-9))
|
|
205
|
+
x1 = min(x1, math.floor(rect.x + rect.width + 1e-9) + 1)
|
|
206
|
+
y0 = max(y0, math.ceil(rect.y - 1e-9))
|
|
207
|
+
y1 = min(y1, math.floor(rect.y + rect.height + 1e-9) + 1)
|
|
208
|
+
if conform == "none" and snapshot.camera_placement is None:
|
|
209
|
+
if source_width > frame.project_width:
|
|
210
|
+
left = _truncated_half(source_width - frame.project_width)
|
|
211
|
+
x0, x1 = max(x0, left), min(x1, left + frame.project_width)
|
|
212
|
+
if source_height > frame.project_height:
|
|
213
|
+
top = _truncated_half(source_height - frame.project_height)
|
|
214
|
+
y0, y1 = max(y0, top), min(y1, top + frame.project_height)
|
|
215
|
+
if (x0, x1, y0, y1) == (0, source_width, 0, source_height):
|
|
216
|
+
return None
|
|
217
|
+
return (max(0, x0), max(0, x1), max(0, y0), max(0, y1))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def uses_exact_aspect_minification(
|
|
221
|
+
snapshot: GeometrySnapshot,
|
|
222
|
+
*,
|
|
223
|
+
frame: FrameGeometry,
|
|
224
|
+
conform: str,
|
|
225
|
+
crop_mode: Optional[str],
|
|
226
|
+
source_is_opaque: bool,
|
|
227
|
+
) -> bool:
|
|
228
|
+
"""Whether Final Cut uses its calibrated whole-raster resize path.
|
|
229
|
+
|
|
230
|
+
The retained Final Cut witnesses use an encoded-domain, support-scaled
|
|
231
|
+
bilinear filter when an opaque decoder raster is reduced to an exactly
|
|
232
|
+
matching Fit / Fill canvas. Crop and Pan create a camera, Trim creates an
|
|
233
|
+
alpha edge, and mismatched aspect ratios create transparent or clipped
|
|
234
|
+
edges, so all of those stay on the general homography sampler.
|
|
235
|
+
|
|
236
|
+
Main callers:
|
|
237
|
+
- ``renderer._FrameComposer.placed`` before linear-light conversion.
|
|
238
|
+
|
|
239
|
+
Why this exists:
|
|
240
|
+
A homography sampler reads only a 2x2 source neighbourhood. That aliases a
|
|
241
|
+
4K or 1080p raster reduced to a small proxy, while Final Cut's resize filter
|
|
242
|
+
widens its footprint as the reduction ratio grows.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
source_width, source_height = frame.source_width, frame.source_height
|
|
246
|
+
output_width, output_height = frame.project_width, frame.project_height
|
|
247
|
+
return (
|
|
248
|
+
source_is_opaque
|
|
249
|
+
and crop_mode is None
|
|
250
|
+
and snapshot.camera_placement is None
|
|
251
|
+
and conform in {"fit", "fill"}
|
|
252
|
+
and output_width >= 3
|
|
253
|
+
and output_height >= 3
|
|
254
|
+
and source_width > output_width
|
|
255
|
+
and source_height > output_height
|
|
256
|
+
and source_width * output_height == source_height * output_width
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
_SWS_H_ALIGN = 4
|
|
261
|
+
_SWS_V_ALIGN = 2
|
|
262
|
+
_SWS_ONE_H = 1 << 14
|
|
263
|
+
_SWS_ONE_V = 1 << 12
|
|
264
|
+
|
|
265
|
+
# Legacy swscale's default RGB matrix when an RGBA link has no colourspace tag.
|
|
266
|
+
# These are ``fill_rgb2yuv_table`` and ``ff_yuv2rgb_c_init_tables`` for
|
|
267
|
+
# SWS_CS_DEFAULT (BT.601), limited range, in FFmpeg n8.0.1.
|
|
268
|
+
_RGB2YUV_601 = (8414, 16519, 3208, -4865, -9528, 14392, 14392, -12061, -2332)
|
|
269
|
+
_YUV2RGB_601 = (8192, 9539, 13075, -6660, -3209, 16525)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@lru_cache(maxsize=256)
|
|
273
|
+
def _swscale_bilinear_filter(
|
|
274
|
+
source_length: int,
|
|
275
|
+
output_length: int,
|
|
276
|
+
*,
|
|
277
|
+
alignment: int,
|
|
278
|
+
one: int,
|
|
279
|
+
) -> tuple[tuple[int, ...], tuple[tuple[int, ...], ...]]:
|
|
280
|
+
"""Port legacy swscale ``initFilter`` for one bilinear minification axis.
|
|
281
|
+
|
|
282
|
+
This includes its 16.16 output-centre positions, support-scaled triangle,
|
|
283
|
+
near-zero tap reduction, SIMD-width padding, border folding, 14/12-bit
|
|
284
|
+
coefficient quantisation, and residual-error diffusion. The returned rows
|
|
285
|
+
each sum to ``one``.
|
|
286
|
+
|
|
287
|
+
An identity axis (``output_length == source_length``, i.e. no scaling on
|
|
288
|
+
that axis) is a special case: swscale performs no resampling there, so we
|
|
289
|
+
short-circuit to a copy filter instead of running the minification maths.
|
|
290
|
+
|
|
291
|
+
Main callers:
|
|
292
|
+
- ``resize_exact_aspect_opaque`` for the horizontal and vertical passes.
|
|
293
|
+
|
|
294
|
+
Why this exists:
|
|
295
|
+
PyTorch's antialiased bilinear kernel has different positions and weights.
|
|
296
|
+
On sharp content that difference is tens of 8-bit codes, so row 23 requires
|
|
297
|
+
swscale's actual legacy coefficients rather than a generic antialias flag.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
# Identity axis: an unscaled axis reaches this builder on exact-2x opaque
|
|
301
|
+
# downscales, where the packed-RGBA path derives a half-width chroma plane
|
|
302
|
+
# (e.g. source 1920 -> chroma 960) whose length already equals the output
|
|
303
|
+
# width (960). swscale does no filtering on an unscaled axis, so the correct
|
|
304
|
+
# behaviour is a straight copy: output sample ``i`` selects source sample
|
|
305
|
+
# ``i`` with the full weight ``one``. Feeding this single-tap-per-row filter
|
|
306
|
+
# through the shared horizontal/vertical gather reproduces the copy exactly
|
|
307
|
+
# while staying in the same fixed-point domain a real minified row uses --
|
|
308
|
+
# every row still sums to ``one``, so the downstream >>13 / >>10 rescale is
|
|
309
|
+
# identical. We must NOT relax the guard below to ``<=`` and run the general
|
|
310
|
+
# maths for this case: at output == source it produces a degenerate,
|
|
311
|
+
# non-identity filter, and at output < 3 the tap geometry is undefined.
|
|
312
|
+
if output_length == source_length:
|
|
313
|
+
positions = tuple(range(output_length))
|
|
314
|
+
rows = tuple((one,) for _ in range(output_length))
|
|
315
|
+
return positions, rows
|
|
316
|
+
|
|
317
|
+
if not (3 <= output_length < source_length):
|
|
318
|
+
raise ValueError(f"legacy bilinear minification requires 3 <= output < source, got {source_length}->{output_length}")
|
|
319
|
+
increment = ((source_length << 16) + (output_length >> 1)) // output_length
|
|
320
|
+
size = 1 + (2 * source_length + output_length - 1) // output_length
|
|
321
|
+
size = max(1, min(size, source_length - 2))
|
|
322
|
+
fone = 1 << (54 - min((source_length // output_length).bit_length() - 1, 8))
|
|
323
|
+
position = ((128 * increment) >> 7) - ((128 * 0x10000) >> 7)
|
|
324
|
+
positions: list[int] = []
|
|
325
|
+
rows: list[list[int]] = []
|
|
326
|
+
|
|
327
|
+
# The bilinear kernel: a support-scaled triangle (``distance`` shrinks by the
|
|
328
|
+
# reduction ratio) quantised to ``fone``. This is the only part that differs
|
|
329
|
+
# from the bicubic port; ``finalize_swscale_filter`` runs the identical
|
|
330
|
+
# reduce / align / border-fix / normalise tail on the raw taps. Bilinear is
|
|
331
|
+
# strictly minifying, so it never needs the size-1 vertical align quirk
|
|
332
|
+
# (``apply_align_quirk=False``) and its historical reform only ever pads
|
|
333
|
+
# (``reform_truncates=False``).
|
|
334
|
+
for _ in range(output_length):
|
|
335
|
+
source_index = _c_div(position - (size - 2) * (1 << 16), 1 << 17)
|
|
336
|
+
positions.append(source_index)
|
|
337
|
+
row: list[int] = []
|
|
338
|
+
for _tap in range(size):
|
|
339
|
+
distance = abs(source_index * (1 << 17) - position) << 13
|
|
340
|
+
distance = distance * output_length // source_length
|
|
341
|
+
coefficient = max(0, (1 << 30) - distance) * (fone >> 30)
|
|
342
|
+
row.append(coefficient)
|
|
343
|
+
source_index += 1
|
|
344
|
+
rows.append(row)
|
|
345
|
+
position += 2 * increment
|
|
346
|
+
|
|
347
|
+
return finalize_swscale_filter(
|
|
348
|
+
positions,
|
|
349
|
+
rows,
|
|
350
|
+
size,
|
|
351
|
+
fone,
|
|
352
|
+
filter_align=alignment,
|
|
353
|
+
one=one,
|
|
354
|
+
apply_align_quirk=False,
|
|
355
|
+
reform_truncates=False,
|
|
356
|
+
src_len=source_length,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _filter_tensors(
|
|
361
|
+
source_length: int,
|
|
362
|
+
output_length: int,
|
|
363
|
+
*,
|
|
364
|
+
alignment: int,
|
|
365
|
+
one: int,
|
|
366
|
+
device: torch.device,
|
|
367
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
368
|
+
positions, taps = _swscale_bilinear_filter(
|
|
369
|
+
source_length,
|
|
370
|
+
output_length,
|
|
371
|
+
alignment=alignment,
|
|
372
|
+
one=one,
|
|
373
|
+
)
|
|
374
|
+
return (
|
|
375
|
+
torch.tensor(positions, dtype=torch.long, device=device),
|
|
376
|
+
torch.tensor(taps, dtype=torch.int32, device=device),
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _floor_shift(values: torch.Tensor, bits: int) -> torch.Tensor:
|
|
381
|
+
return torch.div(values, 1 << bits, rounding_mode="floor")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def resize_exact_aspect_opaque(source_rgb: torch.Tensor, *, height: int, width: int) -> torch.Tensor:
|
|
385
|
+
"""Minify opaque encoded RGB through FFmpeg n8.0.1 legacy swscale bilinear.
|
|
386
|
+
|
|
387
|
+
The port follows the packed-RGBA path, not an independent resize of R, G,
|
|
388
|
+
and B. Legacy swscale first converts RGB to 14-bit BT.601 limited-range
|
|
389
|
+
Y/U/V, derives half-width chroma when reducing by at least 2x, applies its
|
|
390
|
+
14-bit horizontal and 12-bit vertical bilinear filters, then uses the
|
|
391
|
+
fixed-point full-chroma YUV-to-RGB writer. Input and output remain encoded
|
|
392
|
+
8-bit code values, matching the calibrated plan-emitter stage order.
|
|
393
|
+
|
|
394
|
+
Main callers:
|
|
395
|
+
- ``renderer._FrameComposer.placed`` for the bounded exact-aspect case.
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
if source_rgb.ndim != 3 or source_rgb.shape[0] != 3:
|
|
399
|
+
raise ValueError(f"source_rgb must have shape [3,H,W], got {tuple(source_rgb.shape)}")
|
|
400
|
+
_, source_height, source_width = source_rgb.shape
|
|
401
|
+
if not (3 <= width < source_width and 3 <= height < source_height):
|
|
402
|
+
raise ValueError(
|
|
403
|
+
f"resize_exact_aspect_opaque only supports minification with output dimensions >= 3, "
|
|
404
|
+
f"got {source_width}x{source_height}->{width}x{height}"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
code = torch.clamp(torch.round(source_rgb * 255.0), 0.0, 255.0).to(torch.int32)
|
|
408
|
+
red, green, blue = code[0], code[1], code[2]
|
|
409
|
+
ry, gy, by, ru, gu, bu, rv, gv, bv = _RGB2YUV_601
|
|
410
|
+
y14 = _floor_shift(ry * red + gy * green + by * blue + (32 << 14) + (1 << 8), 9)
|
|
411
|
+
|
|
412
|
+
chroma_half = source_width % 2 == 0 and width <= source_width // 2
|
|
413
|
+
if chroma_half:
|
|
414
|
+
red_pair = red[:, 0::2] + red[:, 1::2]
|
|
415
|
+
green_pair = green[:, 0::2] + green[:, 1::2]
|
|
416
|
+
blue_pair = blue[:, 0::2] + blue[:, 1::2]
|
|
417
|
+
u14 = _floor_shift(ru * red_pair + gu * green_pair + bu * blue_pair + (256 << 15) + (1 << 9), 10)
|
|
418
|
+
v14 = _floor_shift(rv * red_pair + gv * green_pair + bv * blue_pair + (256 << 15) + (1 << 9), 10)
|
|
419
|
+
chroma_width = source_width // 2
|
|
420
|
+
else:
|
|
421
|
+
u14 = _floor_shift(ru * red + gu * green + bu * blue + (256 << 14) + (1 << 8), 9)
|
|
422
|
+
v14 = _floor_shift(rv * red + gv * green + bv * blue + (256 << 14) + (1 << 8), 9)
|
|
423
|
+
chroma_width = source_width
|
|
424
|
+
|
|
425
|
+
h_positions, h_taps = _filter_tensors(
|
|
426
|
+
source_width, width, alignment=_SWS_H_ALIGN, one=_SWS_ONE_H, device=source_rgb.device
|
|
427
|
+
)
|
|
428
|
+
c_positions, c_taps = _filter_tensors(
|
|
429
|
+
chroma_width, width, alignment=_SWS_H_ALIGN, one=_SWS_ONE_H, device=source_rgb.device
|
|
430
|
+
)
|
|
431
|
+
v_positions, v_taps = _filter_tensors(
|
|
432
|
+
source_height, height, alignment=_SWS_V_ALIGN, one=_SWS_ONE_V, device=source_rgb.device
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
def horizontal(plane14: torch.Tensor, positions: torch.Tensor, taps: torch.Tensor) -> torch.Tensor:
|
|
436
|
+
value = torch.zeros((source_height, width), dtype=torch.int32, device=source_rgb.device)
|
|
437
|
+
for tap_index in range(taps.shape[1]):
|
|
438
|
+
indices = torch.clamp(positions + tap_index, max=plane14.shape[1] - 1)
|
|
439
|
+
value += torch.index_select(plane14, 1, indices) * taps[:, tap_index].view(1, -1)
|
|
440
|
+
return torch.clamp(_floor_shift(value, 13), max=32767)
|
|
441
|
+
|
|
442
|
+
horizontal_y = horizontal(y14, h_positions, h_taps)
|
|
443
|
+
horizontal_u = horizontal(u14, c_positions, c_taps)
|
|
444
|
+
horizontal_v = horizontal(v14, c_positions, c_taps)
|
|
445
|
+
|
|
446
|
+
def vertical(plane15: torch.Tensor, *, center_chroma: bool) -> torch.Tensor:
|
|
447
|
+
initial = (1 << 9) - (128 << 19) if center_chroma else (1 << 9)
|
|
448
|
+
value = torch.full((height, width), initial, dtype=torch.int32, device=source_rgb.device)
|
|
449
|
+
for tap_index in range(v_taps.shape[1]):
|
|
450
|
+
indices = torch.clamp(v_positions + tap_index, max=source_height - 1)
|
|
451
|
+
value += torch.index_select(plane15, 0, indices) * v_taps[:, tap_index].view(-1, 1)
|
|
452
|
+
return _floor_shift(value, 10)
|
|
453
|
+
|
|
454
|
+
y17 = vertical(horizontal_y, center_chroma=False)
|
|
455
|
+
u17 = vertical(horizontal_u, center_chroma=True)
|
|
456
|
+
v17 = vertical(horizontal_v, center_chroma=True)
|
|
457
|
+
y_offset, y_coeff, v2r, v2g, u2g, u2b = _YUV2RGB_601
|
|
458
|
+
luma = (y17 - y_offset) * y_coeff + (1 << 21)
|
|
459
|
+
limit = (1 << 30) - 1
|
|
460
|
+
result = torch.stack(
|
|
461
|
+
(
|
|
462
|
+
torch.clamp(luma + v17 * v2r, 0, limit),
|
|
463
|
+
torch.clamp(luma + v17 * v2g + u17 * u2g, 0, limit),
|
|
464
|
+
torch.clamp(luma + u17 * u2b, 0, limit),
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
return _floor_shift(result, 22).to(source_rgb.dtype) / 255.0
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def apply_display_rotation(source: torch.Tensor, rotation_degrees: int) -> torch.Tensor:
|
|
471
|
+
"""Apply container display rotation to ``[C,H,W]`` before timeline geometry.
|
|
472
|
+
|
|
473
|
+
FFmpeg's autorotate and Final Cut interpret a positive display-matrix angle
|
|
474
|
+
as a counter-clockwise image rotation. ``torch.rot90`` uses that same sign.
|
|
475
|
+
|
|
476
|
+
Main callers:
|
|
477
|
+
- ``renderer._FrameComposer.placed`` immediately after source decode.
|
|
478
|
+
"""
|
|
479
|
+
|
|
480
|
+
normalized = rotation_degrees % 360
|
|
481
|
+
if normalized not in {0, 90, 180, 270}:
|
|
482
|
+
raise ValueError(f"display rotation must be a quarter turn, got {rotation_degrees}")
|
|
483
|
+
return source if normalized == 0 else torch.rot90(source, k=normalized // 90, dims=(-2, -1))
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# --------------------------------------------------------------------------- torch
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class GridCache:
|
|
490
|
+
"""Homogeneous output pixel-centre grids per (height, width, device), plus per-key grids.
|
|
491
|
+
|
|
492
|
+
``grid_for(key, matrix, ...)`` returns the normalized sampling grid for
|
|
493
|
+
``matrix`` and reuses the previous one while the matrix is unchanged --
|
|
494
|
+
static layers pay the grid build once, animated layers once per frame.
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
def __init__(self) -> None:
|
|
498
|
+
self._centres: dict[tuple[int, int, str], torch.Tensor] = {}
|
|
499
|
+
self._grids: dict[str, tuple[bytes, torch.Tensor]] = {}
|
|
500
|
+
|
|
501
|
+
def centres(self, height: int, width: int, device: torch.device) -> torch.Tensor:
|
|
502
|
+
key = (height, width, str(device))
|
|
503
|
+
centres = self._centres.get(key)
|
|
504
|
+
if centres is None:
|
|
505
|
+
ys = torch.arange(height, dtype=torch.float32, device=device) + 0.5
|
|
506
|
+
xs = torch.arange(width, dtype=torch.float32, device=device) + 0.5
|
|
507
|
+
grid_y, grid_x = torch.meshgrid(ys, xs, indexing="ij")
|
|
508
|
+
centres = torch.stack((grid_x, grid_y, torch.ones_like(grid_x)), dim=-1).reshape(-1, 3)
|
|
509
|
+
self._centres[key] = centres
|
|
510
|
+
return centres
|
|
511
|
+
|
|
512
|
+
def grid_for(
|
|
513
|
+
self,
|
|
514
|
+
key: str,
|
|
515
|
+
matrix: Matrix,
|
|
516
|
+
*,
|
|
517
|
+
out_height: int,
|
|
518
|
+
out_width: int,
|
|
519
|
+
source_height: int,
|
|
520
|
+
source_width: int,
|
|
521
|
+
device: torch.device,
|
|
522
|
+
) -> torch.Tensor:
|
|
523
|
+
signature = np.ascontiguousarray(matrix, dtype=np.float64).tobytes() + bytes(
|
|
524
|
+
f"{out_height}x{out_width}<{source_height}x{source_width}", "ascii"
|
|
525
|
+
)
|
|
526
|
+
cached = self._grids.get(key)
|
|
527
|
+
if cached is not None and cached[0] == signature:
|
|
528
|
+
return cached[1]
|
|
529
|
+
grid = sampling_grid(
|
|
530
|
+
matrix,
|
|
531
|
+
centres=self.centres(out_height, out_width, device),
|
|
532
|
+
out_height=out_height,
|
|
533
|
+
out_width=out_width,
|
|
534
|
+
source_height=source_height,
|
|
535
|
+
source_width=source_width,
|
|
536
|
+
)
|
|
537
|
+
self._grids[key] = (signature, grid)
|
|
538
|
+
return grid
|
|
539
|
+
|
|
540
|
+
def forget(self, key: str) -> None:
|
|
541
|
+
self._grids.pop(key, None)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def sampling_grid(
|
|
545
|
+
matrix: Matrix,
|
|
546
|
+
*,
|
|
547
|
+
centres: torch.Tensor,
|
|
548
|
+
out_height: int,
|
|
549
|
+
out_width: int,
|
|
550
|
+
source_height: int,
|
|
551
|
+
source_width: int,
|
|
552
|
+
) -> torch.Tensor:
|
|
553
|
+
"""Inverse-map output pixel centres through ``matrix`` into normalized source coords.
|
|
554
|
+
|
|
555
|
+
``matrix`` maps source edge coords to output edge coords; the grid holds
|
|
556
|
+
the source position of every output pixel centre, normalized so that the
|
|
557
|
+
source raster spans ``[-1, 1]`` (``align_corners=False``). Solved in
|
|
558
|
+
float64 on the CPU for the 3x3 inverse, evaluated in float32 on device
|
|
559
|
+
(fp16 grids are ~1 px wrong; never use them).
|
|
560
|
+
"""
|
|
561
|
+
|
|
562
|
+
inverse = np.linalg.inv(matrix)
|
|
563
|
+
inverse_t = torch.from_numpy(inverse.T.astype(np.float32)).to(centres.device)
|
|
564
|
+
mapped = centres @ inverse_t # [N, 3]
|
|
565
|
+
w = mapped[:, 2:3]
|
|
566
|
+
# A point behind the camera / at infinity (degenerate perspective) is sent
|
|
567
|
+
# far outside the source so zeros padding makes it transparent.
|
|
568
|
+
safe_w = torch.where(w.abs() < 1e-12, torch.full_like(w, 1e-12), w)
|
|
569
|
+
u = mapped[:, 0:1] / safe_w
|
|
570
|
+
v = mapped[:, 1:2] / safe_w
|
|
571
|
+
grid_x = u * (2.0 / source_width) - 1.0
|
|
572
|
+
grid_y = v * (2.0 / source_height) - 1.0
|
|
573
|
+
return torch.cat((grid_x, grid_y), dim=1).reshape(1, out_height, out_width, 2)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def warp(source_rgba: torch.Tensor, grid: torch.Tensor) -> torch.Tensor:
|
|
577
|
+
"""Bilinear straight-RGBA warp with a transparent border; ``[4,h,w]`` -> ``[4,H,W]``."""
|
|
578
|
+
|
|
579
|
+
return F.grid_sample(
|
|
580
|
+
source_rgba.unsqueeze(0), grid, mode="bilinear", padding_mode="zeros", align_corners=False
|
|
581
|
+
).squeeze(0)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def apply_alpha_window(alpha: torch.Tensor, window: Optional[tuple[int, int, int, int]]) -> torch.Tensor:
|
|
585
|
+
"""Zero ``alpha`` (``[1,h,w]``) outside the half-open pixel-index window."""
|
|
586
|
+
|
|
587
|
+
if window is None:
|
|
588
|
+
return alpha
|
|
589
|
+
x0, x1, y0, y1 = window
|
|
590
|
+
masked = torch.zeros_like(alpha)
|
|
591
|
+
if x1 > x0 and y1 > y0:
|
|
592
|
+
masked[:, y0:y1, x0:x1] = alpha[:, y0:y1, x0:x1]
|
|
593
|
+
return masked
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def premultiply(straight_rgba: torch.Tensor) -> torch.Tensor:
|
|
597
|
+
alpha = straight_rgba[3:4]
|
|
598
|
+
return torch.cat((straight_rgba[:3] * alpha, alpha), dim=0)
|