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,851 @@
|
|
|
1
|
+
"""E4 effect ports: the colour pipeline -- Color Adjustments and Color Board (owner: E4 batch).
|
|
2
|
+
|
|
3
|
+
Architecture map
|
|
4
|
+
================
|
|
5
|
+
|
|
6
|
+
plan.build_tensor_plan -> effects.lower_effect -> _lower_color(effect, ctx)
|
|
7
|
+
color.color_effect_filters(effect) : the CPU emitter's EXACT filter strings
|
|
8
|
+
(same parameter lookup, clamps, ``.12g``
|
|
9
|
+
formatting the ffmpeg CLI would parse)
|
|
10
|
+
ctx.reference_effect_link : which pixel link the reference feeds the
|
|
11
|
+
stack through (``BridgeLink``, computed by
|
|
12
|
+
the plan via ``reference_effect_link``)
|
|
13
|
+
_parse_stage(filter_string, link) : one ffmpeg filter -> one frozen stage
|
|
14
|
+
(unknown filter / option -> loud reject)
|
|
15
|
+
ColorPipelinePayload(stages) : the port payload (hashable, no torch)
|
|
16
|
+
|
|
17
|
+
renderer._FrameComposer.placed -> effects.apply_effects -> _apply_color(payload, canvas)
|
|
18
|
+
color.premultiplied_to_code : premultiplied linear -> straight 0..255 code
|
|
19
|
+
round to the uint8 frame the reference sees (``format=rgba``)
|
|
20
|
+
stages in order:
|
|
21
|
+
YuvBridgeStage : swscale RGB -> yuva444p (8-bit, lossy; matrix / range / alpha
|
|
22
|
+
path per the negotiated ``BridgeLink``) -> ``eq`` per-plane
|
|
23
|
+
LUTs -> swscale yuva444p -> RGB
|
|
24
|
+
LutStage : per-channel 8-bit LUT (``colorlevels``, ``curves``)
|
|
25
|
+
ColorBalanceStage: ``colorbalance`` float32 three-zone math (``pl=0``)
|
|
26
|
+
color.code_to_premultiplied : straight code -> premultiplied linear
|
|
27
|
+
|
|
28
|
+
Reference filter chains (``src/bladeworks/core/color.py``; ffmpeg 8.0.1 auto-negotiates
|
|
29
|
+
the pixel formats, ``ffmpeg -v verbose`` shows the two ``auto_scale`` insertions):
|
|
30
|
+
|
|
31
|
+
Color Adjustments (always emits ``eq``):
|
|
32
|
+
RGB --auto_scale--> yuva444p eq=brightness:contrast:saturation:gamma
|
|
33
|
+
--auto_scale--> RGB [colorlevels=rimin=gimin=bimin=k] (black point > 0)
|
|
34
|
+
[colorbalance=rs:bs:rm:bm:rh:bh:gs:gm:gh] (warmth/tint)
|
|
35
|
+
Color Board (every filter optional):
|
|
36
|
+
[eq=saturation=s] (same yuva444p bridge) [curves=master='5 points'] [colorbalance=...]
|
|
37
|
+
|
|
38
|
+
Which bridge: the negotiated ``eq`` link (documented here so E1/E2 can reuse the pattern)
|
|
39
|
+
--------------------------------------------------------------------------------------
|
|
40
|
+
``eq`` only accepts 8-bit YUV formats, so libavfilter inserts ``scale`` (libswscale) around
|
|
41
|
+
it. What that scale computes depends on three facts of the *reference graph*, all of which
|
|
42
|
+
were read off ``ffmpeg -v verbose`` on real reference graphs and are modelled by
|
|
43
|
+
``BridgeLink`` = ``<pix_fmt>:<colorspace>:<range>``:
|
|
44
|
+
|
|
45
|
+
* ``pix_fmt`` of the RGB frame entering ``eq``: ``rgba`` after a bare ``format=rgba`` chain
|
|
46
|
+
(same-size clip, trim crop, ``conform none``, folded group effects) or ``gbrap`` after a
|
|
47
|
+
``perspective`` stage (conform resample, camera crop, Ken Burns) -- ``perspective`` only
|
|
48
|
+
supports 8-bit planar formats. It decides the alpha path (``rgbaToA_c`` replicates bit 7:
|
|
49
|
+
A >= 128 gains one code; ``planar_rgb_to_a`` is exact).
|
|
50
|
+
* ``colorspace`` of the YUV link: libavfilter propagates the *source stream's* colorspace tag
|
|
51
|
+
through the RGB filters to the YUV link -- ``bt709`` for tagged camera media, unknown /
|
|
52
|
+
``gbr`` (PNG rasters, titles) or ``bt470bg`` (JPEG) -> the ``SWS_CS_DEFAULT`` (601) table.
|
|
53
|
+
Others (bt2020nc, smpte240m, fcc, ycgco) are not ported and reject loudly.
|
|
54
|
+
* ``range`` of the YUV link: the source's range tag (``tv`` / unknown -> limited; ``pc`` for
|
|
55
|
+
JPEG / yuvj sources -> full), or ``pc`` whenever ``setparams=range=full`` (every
|
|
56
|
+
``perspective`` stage) precedes the effects.
|
|
57
|
+
|
|
58
|
+
``reference_effect_link(...)`` derives the link from plan facts; the plan passes it as
|
|
59
|
+
``LowerContext.reference_effect_link``. All eight links were verified bit-exact against
|
|
60
|
+
``ffmpeg`` on all 2**24 RGB triples and all 2**24 YUV triples (``probe_swscale`` run,
|
|
61
|
+
2026-08-17; the test file re-checks each on a 1M-pixel plate). No dithering anywhere (8-bit
|
|
62
|
+
source: ``ff_sws_pb_64`` -> ``+64 >> 7``).
|
|
63
|
+
|
|
64
|
+
RGB -> YUV (``input.c`` ``rgb32ToY_c`` / ``rgb32ToUV_c`` = ``planar_rgb_to_y`` / ``_uv``,
|
|
65
|
+
``hScale16To15`` (x2, FFMIN 32767), ``lumRangeToJpeg_c`` / ``chrRangeToJpeg_c`` for ``pc``,
|
|
66
|
+
``yuv2plane1_8_c``):
|
|
67
|
+
|
|
68
|
+
RY..BV = rgb2yuv_table(colorspace) # fill_rgb2yuv_table (utils.c), 15-bit, ROUNDED_DIV
|
|
69
|
+
bt601: 8414 16519 3208 | -4865 -9528 14392 | 14392 -12061 -2332 (fixed constants)
|
|
70
|
+
bt709: 5983 20127 2032 | -3298 -11094 14392 | 14392 -13073 -1320
|
|
71
|
+
y14 = (RY*R + GY*G + BY*B + (32 << 14) + (1 << 8)) >> 9 # 15-bit sum -> 14-bit, offset 16
|
|
72
|
+
u14 = (RU*R + GU*G + BU*B + (256 << 14) + (1 << 8)) >> 9 # offset 128
|
|
73
|
+
v14 = (RV*R + GV*G + BV*B + (256 << 14) + (1 << 8)) >> 9
|
|
74
|
+
x15 = min(2*x14, 32767)
|
|
75
|
+
pc: x15 = min((x15*coeff + offset) >> 14, 32767) with (coeff, offset) = solve_range_convert
|
|
76
|
+
(n8.0.1: luma (19078, -39084288), chroma (18652, -38207488); n8.0 lacked the +2**13
|
|
77
|
+
rounding term and lands 0.4% of codes one lower)
|
|
78
|
+
X8 = clip((x15 + 64) >> 7)
|
|
79
|
+
A8' = rgba: min(A8 + (A8 >= 128), 255) ; gbrap: A8
|
|
80
|
+
|
|
81
|
+
YUV -> RGB (``output.c`` ``yuv2rgb_write_full`` for ``rgba``, ``yuv2gbrp_full_X_c`` for
|
|
82
|
+
``gbrap`` -- identical arithmetic; ``ff_yuv2rgb_c_init_tables`` per (colorspace, range)):
|
|
83
|
+
|
|
84
|
+
(y_coeff, y_off>>9, v2r, v2g, u2g, u2b) = yuv2rgb_coefficients(colorspace, full_range)
|
|
85
|
+
bt601 tv: 9539 16 13075 -6660 -3209 16525 bt601 pc: 8192 0 11485 -5850 -2819 14516
|
|
86
|
+
bt709 tv: 9539 16 14686 -4366 -1747 17305 bt709 pc: 8192 0 12901 -3835 -1534 15201
|
|
87
|
+
Y = Y8 << 9 ; U = (U8 - 128) << 9 ; V = (V8 - 128) << 9
|
|
88
|
+
t = (Y - (y_off << 9)) * y_coeff + (1 << 21)
|
|
89
|
+
R = t + V*v2r ; G = t + V*v2g + U*u2g ; B = t + U*u2b (32-bit int arithmetic)
|
|
90
|
+
if (R|G|B) & 0xC0000000: each is av_clip_uintp2(x, 30) -> negative OR wrapped-past-2**31 -> 0
|
|
91
|
+
R8 = R >> 22 (etc.); A8 copied exactly.
|
|
92
|
+
In the >>9-reduced form used here: T = (Y8-y_off)*y_coeff + 4096 + chroma terms;
|
|
93
|
+
R8 = 0 if T < 0 or T >= 2**22 (the B plane really reaches the 2**31 wrap for Y8=255, U8=255
|
|
94
|
+
in the limited sets and swscale returns 0 there -- reproduced, not "fixed"), 255 if
|
|
95
|
+
T >= 2**21, else T >> 13.
|
|
96
|
+
|
|
97
|
+
Post-``eq`` precision: when the effect stack is followed by a spatial tail (static transform /
|
|
98
|
+
corner pin: ``format=rgba64le,lutrgb...``), libavfilter negotiates the post-``eq`` RGB link to
|
|
99
|
+
``rgba64le`` and ``colorlevels`` / ``colorbalance`` / ``curves`` run at 16 bits until the
|
|
100
|
+
tail's ``perspective`` quantises to 8-bit ``gbrap``; the port always runs them at 8 bits (the
|
|
101
|
+
no-tail negotiation), which is within one code of that -- soft-match territory, recorded in
|
|
102
|
+
the E4 report as a ledger candidate.
|
|
103
|
+
|
|
104
|
+
``eq`` (``vf_eq.c``, per plane, alpha plane copied): luma uses ``process_c`` when gamma == 1
|
|
105
|
+
(``c = int(contrast*4096)``, ``b = ((int)(100*brightness+100)*511)/200 - 128 - c/32`` in C
|
|
106
|
+
(truncating) division, controls float32-rounded like ``av_clipf`` -- ``eq_process_lut``,
|
|
107
|
+
``pel = clip(((src*c) >> 12) + b)``) and the double LUT ``256.0 * pow(contrast*(v-0.5)+0.5+
|
|
108
|
+
brightness, 1/gamma)`` (truncated, 0 for v <= 0, 255 for v >= 1) otherwise; chroma uses
|
|
109
|
+
``process_c`` with ``contrast = saturation`` and ``brightness = 0``, or is copied when
|
|
110
|
+
saturation == 1.0. All three are 8-bit LUTs, computed at plan time in Python integers /
|
|
111
|
+
doubles. The C x86 SIMD path (``psllw 4; pmulhw``) equals ``process_c`` bit for bit.
|
|
112
|
+
|
|
113
|
+
``colorlevels`` (8-bit RGB, packed or planar): ``imin = lrint(k*255)``, ``coeff = float32(255/(255-imin))``,
|
|
114
|
+
``dst = clip(trunc(float32(src - imin) * coeff))``; alpha coeff 1.0 -> copied.
|
|
115
|
+
``colorbalance`` (8-bit RGB, float32, ``pl=0``): ``l = max(r,g,b) + min(r,g,b)`` on ``x/255``;
|
|
116
|
+
zone weights ``clipf((0.333 - l)*4 + 0.5) * 0.7`` (shadows), ``clipf((l-0.333)*4+0.5) *
|
|
117
|
+
clipf((1-l-0.333)*4+0.5) * 0.7`` (midtones), ``clipf((l+0.333-1)*4+0.5) * 0.7`` (highlights);
|
|
118
|
+
``v = clipf(v + s + m + h)``; ``lrintf(v*255)`` (half-to-even); alpha copied. The
|
|
119
|
+
reference binary contracts ``x*a + 0.5f`` into FMA on arm64, so this stage is <= 1 code, not
|
|
120
|
+
bit-exact, and the goldens gate it at 1.
|
|
121
|
+
``curves=master=...`` (8-bit RGB): natural cubic spline through the points in double
|
|
122
|
+
(``interpolate()``), ``clip((int)(y*255))`` per code, applied to R, G, B; alpha copied.
|
|
123
|
+
|
|
124
|
+
Precision contract: every stage is 8-bit-in / 8-bit-out exactly like the reference link
|
|
125
|
+
between filters (the reference's bridge loss IS the calibrated look and is reproduced, not
|
|
126
|
+
avoided); the input frame is the working canvas' straight code rounded to the nearest
|
|
127
|
+
integer (the uint8 ``format=rgba`` / ``gbrap`` frame the reference feeds ``eq``). Integer
|
|
128
|
+
stages run in int32 on the canvas device (MPS-safe: no int64, no float64).
|
|
129
|
+
|
|
130
|
+
Main callers:
|
|
131
|
+
- ``effects.lower_effect`` / ``effects.apply_effects`` through the registry
|
|
132
|
+
(``EFFECT_PORTS["color_adjustments"]``, ``EFFECT_PORTS["color_board"]``).
|
|
133
|
+
|
|
134
|
+
Why this exists:
|
|
135
|
+
Color Adjustments is the one calibrated colour handler in the corpus, and its look is
|
|
136
|
+
inseparable from the accidental 8-bit YUV bridge the CPU graph pays for ``eq``. A
|
|
137
|
+
"cleaner" float port would not soft-match the reference (the oracle in
|
|
138
|
+
``evidence/color_adjustments_shared_plan_v3.json`` is +-1 code against that bridge), so
|
|
139
|
+
the port reproduces libswscale / libavfilter integer semantics exactly and keeps the whole
|
|
140
|
+
parameter lowering on the CPU emitter (``color.color_effect_filters``) so both backends can
|
|
141
|
+
never disagree about what the authored controls mean.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
from __future__ import annotations
|
|
145
|
+
|
|
146
|
+
import math
|
|
147
|
+
from dataclasses import dataclass
|
|
148
|
+
from functools import lru_cache
|
|
149
|
+
from typing import Final, Mapping, Sequence
|
|
150
|
+
|
|
151
|
+
import numpy as np
|
|
152
|
+
import torch
|
|
153
|
+
|
|
154
|
+
from ..core.color import color_effect_filters
|
|
155
|
+
from ..core.model import ResolvedEffect
|
|
156
|
+
from .color import code_to_premultiplied, premultiplied_to_code
|
|
157
|
+
from .effects import ApplyContext, EffectPort, LowerContext, register
|
|
158
|
+
from .support import reject
|
|
159
|
+
|
|
160
|
+
# --------------------------------------------------------------------------- swscale model
|
|
161
|
+
#
|
|
162
|
+
# The pixel link libavfilter negotiates around ``eq`` (see the module doc "which bridge"):
|
|
163
|
+
# input pixel format (alpha path), the YUV colour matrix (the *source stream's* colorspace
|
|
164
|
+
# tag propagates to the YUV link) and the YUV range (source range tag, or ``pc`` after
|
|
165
|
+
# ``setparams=range=full``). Everything below is libswscale n8.0.1 integer arithmetic,
|
|
166
|
+
# verified bit-exact against ``ffmpeg`` on all 2**24 triples for every variant.
|
|
167
|
+
|
|
168
|
+
LINK_FORMATS: Final = ("rgba", "gbrap")
|
|
169
|
+
LINK_COLORSPACES: Final = ("bt601", "bt709") # bt601 = the SWS_CS_DEFAULT table (unknown / gbr / bt470bg / smpte170m)
|
|
170
|
+
LINK_RANGES: Final = ("tv", "pc")
|
|
171
|
+
|
|
172
|
+
# ff_yuv2rgb_coeffs (yuv2rgb.c): {crv, cbu, cgu, cgv} per colourspace; the 601 row is SWS_CS_DEFAULT.
|
|
173
|
+
_CSP_TABLES: Final[Mapping[str, tuple[int, int, int, int]]] = {
|
|
174
|
+
"bt601": (104597, 132201, 25675, 53279),
|
|
175
|
+
"bt709": (117489, 138438, 13975, 34925),
|
|
176
|
+
}
|
|
177
|
+
# ffprobe / libavfilter colourspace names -> the swscale table they select (sws_getCoefficients:
|
|
178
|
+
# RGB(0) / unspecified(2) / reserved(3) / bt470bg(5) / smpte170m(6) / >10 all use the default row).
|
|
179
|
+
LINK_COLORSPACE_ALIASES: Final[Mapping[str, str]] = {
|
|
180
|
+
"bt709": "bt709",
|
|
181
|
+
"bt601": "bt601", "bt470bg": "bt601", "smpte170m": "bt601", "unknown": "bt601",
|
|
182
|
+
"unspecified": "bt601", "reserved": "bt601", "gbr": "bt601", "rgb": "bt601",
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass(frozen=True)
|
|
187
|
+
class BridgeLink:
|
|
188
|
+
"""The negotiated ``eq`` link: ``<pix_fmt>:<colorspace>:<range>``, e.g. ``rgba:bt709:tv``."""
|
|
189
|
+
|
|
190
|
+
pix_fmt: str # "rgba" (rgbaToA_c alpha quirk) | "gbrap" (alpha exact)
|
|
191
|
+
colorspace: str # "bt601" | "bt709"
|
|
192
|
+
color_range: str # "tv" (limited, also unknown) | "pc" (full)
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def parse(text: str) -> "BridgeLink":
|
|
196
|
+
"""``<pix_fmt>:<colorspace>:<range>`` -> link; ``ValueError`` names what is not portable."""
|
|
197
|
+
|
|
198
|
+
parts = text.split(":")
|
|
199
|
+
if len(parts) != 3:
|
|
200
|
+
raise ValueError(f"reference effect link {text!r} is not '<pix_fmt>:<colorspace>:<range>'")
|
|
201
|
+
pix_fmt, colorspace, color_range = parts
|
|
202
|
+
if pix_fmt not in LINK_FORMATS:
|
|
203
|
+
raise ValueError(f"reference effect link {text!r}: pixel format {pix_fmt!r} not in {LINK_FORMATS}")
|
|
204
|
+
if colorspace not in LINK_COLORSPACES:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"reference effect link {text!r}: colourspace {colorspace!r} is not one of the ported eq "
|
|
207
|
+
f"bridge matrices {LINK_COLORSPACES} (aliases {sorted(set(LINK_COLORSPACE_ALIASES))})"
|
|
208
|
+
)
|
|
209
|
+
if color_range not in LINK_RANGES:
|
|
210
|
+
raise ValueError(f"reference effect link {text!r}: range {color_range!r} not in {LINK_RANGES}")
|
|
211
|
+
return BridgeLink(pix_fmt, colorspace, color_range)
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def full_range(self) -> bool:
|
|
215
|
+
return self.color_range == "pc"
|
|
216
|
+
|
|
217
|
+
def __str__(self) -> str:
|
|
218
|
+
return f"{self.pix_fmt}:{self.colorspace}:{self.color_range}"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _c_div(a: int, b: int) -> int:
|
|
222
|
+
"""C integer division (truncates toward zero)."""
|
|
223
|
+
|
|
224
|
+
q = abs(a) // abs(b)
|
|
225
|
+
return q if (a >= 0) == (b > 0) else -q
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _rounded_div(a: int, b: int) -> int:
|
|
229
|
+
"""FFmpeg ``ROUNDED_DIV`` (b > 0): ``(a +- b/2) / b`` with C truncation."""
|
|
230
|
+
|
|
231
|
+
return _c_div(a + (b >> 1) if a >= 0 else a - (b >> 1), b)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _round_to_int16(value: int) -> int:
|
|
235
|
+
"""``roundToInt16`` (yuv2rgb.c) without the +-0x7FFF saturation (never reached here)."""
|
|
236
|
+
|
|
237
|
+
return (value + (1 << 15)) >> 16
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@lru_cache(maxsize=None)
|
|
241
|
+
def rgb2yuv_table(colorspace: str) -> tuple[int, int, int, int, int, int, int, int, int]:
|
|
242
|
+
"""``fill_rgb2yuv_table`` (utils.c): (RY, GY, BY, RU, GU, BU, RV, GV, BV), 15-bit fixed point.
|
|
243
|
+
|
|
244
|
+
``dstRange`` is forced to 0 in that function (range conversion happens elsewhere), so the
|
|
245
|
+
table is always the limited-range one; the default (601) table is overwritten with the
|
|
246
|
+
fixed ``(int)(coef * 219/255 [Y] or 224/255 [UV] * 32768 + 0.5)`` constants.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
table = _CSP_TABLES[colorspace]
|
|
250
|
+
vr, ub, ug, vg = table[0], table[1], -table[2], -table[3]
|
|
251
|
+
one = 65536
|
|
252
|
+
cy = one * 255 // 219
|
|
253
|
+
w = _rounded_div(one * one * ug, ub)
|
|
254
|
+
v = _rounded_div(one * one * vg, vr)
|
|
255
|
+
z = one * one - w - v
|
|
256
|
+
c_y, c_u, c_v = _rounded_div(cy * z, one), _rounded_div(ub * z, one), _rounded_div(vr * z, one)
|
|
257
|
+
s = 1 << 15
|
|
258
|
+
ry, gy, by = -_rounded_div(s * v, c_y), _rounded_div(s * one * one, c_y), -_rounded_div(s * w, c_y)
|
|
259
|
+
ru, gu, bu = _rounded_div(s * v, c_u), -_rounded_div(s * one * one, c_u), _rounded_div(s * (z + w), c_u)
|
|
260
|
+
rv, gv, bv = _rounded_div(s * (v + z), c_v), -_rounded_div(s * one * one, c_v), _rounded_div(s * w, c_v)
|
|
261
|
+
if table == _CSP_TABLES["bt601"]:
|
|
262
|
+
by, bv, bu = int(0.114 * 219 / 255 * s + 0.5), -int(0.081 * 224 / 255 * s + 0.5), int(0.500 * 224 / 255 * s + 0.5)
|
|
263
|
+
gy, gv, gu = int(0.587 * 219 / 255 * s + 0.5), -int(0.419 * 224 / 255 * s + 0.5), -int(0.331 * 224 / 255 * s + 0.5)
|
|
264
|
+
ry, rv, ru = int(0.299 * 219 / 255 * s + 0.5), int(0.500 * 224 / 255 * s + 0.5), -int(0.169 * 224 / 255 * s + 0.5)
|
|
265
|
+
return ry, gy, by, ru, gu, bu, rv, gv, bv
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@dataclass(frozen=True)
|
|
269
|
+
class _Yuv2Rgb:
|
|
270
|
+
"""``ff_yuv2rgb_c_init_tables`` (yuv2rgb.c) for one (colourspace, source range): the six int16
|
|
271
|
+
coefficients ``yuv2rgb_write_full`` / ``yuv2gbrp_full_X_c`` use (brightness 0, contrast 1,
|
|
272
|
+
saturation 1)."""
|
|
273
|
+
|
|
274
|
+
y_coeff: int
|
|
275
|
+
y_offset: int # already >> 9 (the port works on the accumulator >> 9)
|
|
276
|
+
v2r: int
|
|
277
|
+
v2g: int
|
|
278
|
+
u2g: int
|
|
279
|
+
u2b: int
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@lru_cache(maxsize=None)
|
|
283
|
+
def yuv2rgb_coefficients(colorspace: str, full_range: bool) -> _Yuv2Rgb:
|
|
284
|
+
"""``ff_yuv2rgb_c_init_tables`` for ``ff_yuv2rgb_coeffs[colorspace]`` and the YUV source range."""
|
|
285
|
+
|
|
286
|
+
crv, cbu, cgu, cgv = _CSP_TABLES[colorspace]
|
|
287
|
+
cgu, cgv = -cgu, -cgv
|
|
288
|
+
cy, oy = 1 << 16, 0
|
|
289
|
+
if full_range:
|
|
290
|
+
crv, cbu, cgu, cgv = (_c_div(v * 224, 255) for v in (crv, cbu, cgu, cgv))
|
|
291
|
+
else:
|
|
292
|
+
cy = (cy * 255) // 219
|
|
293
|
+
oy = 16 << 16
|
|
294
|
+
return _Yuv2Rgb(
|
|
295
|
+
y_coeff=_round_to_int16(cy << 13),
|
|
296
|
+
y_offset=_round_to_int16(oy << 9) >> 9,
|
|
297
|
+
v2r=_round_to_int16(crv << 13), v2g=_round_to_int16(cgv << 13),
|
|
298
|
+
u2g=_round_to_int16(cgu << 13), u2b=_round_to_int16(cbu << 13),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _solve_range_convert(src_min: int, src_max: int, dst_min: int, dst_max: int) -> tuple[int, int]:
|
|
303
|
+
"""``solve_range_convert`` (swscale.c, n8.0.1) for an 8-bit destination: 15-bit source,
|
|
304
|
+
``src_shift`` 7, ``mult_shift`` 14; the ``+ (1 << 13)`` rounding term in the offset is
|
|
305
|
+
the n8.0.1 fix (n8.0 lacked it and lands 0.4% of codes one lower)."""
|
|
306
|
+
|
|
307
|
+
src_shift, mult_shift = 7, 14
|
|
308
|
+
total_shift = mult_shift + src_shift
|
|
309
|
+
coeff = -((-(((dst_max - dst_min) << total_shift) // (src_max - src_min))) >> src_shift)
|
|
310
|
+
offset = (dst_max << total_shift) - (src_max << src_shift) * coeff + (1 << (mult_shift - 1))
|
|
311
|
+
return coeff, offset
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
_LUM_TO_JPEG: Final = _solve_range_convert(16, 235, 0, 255) # (19078, -39084288)
|
|
315
|
+
_CHR_TO_JPEG: Final = _solve_range_convert(16, 240, 0, 255) # (18652, -38207488)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# --------------------------------------------------------------------------- payload types
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@dataclass(frozen=True)
|
|
322
|
+
class YuvBridgeStage:
|
|
323
|
+
"""RGB -> yuva444p -> ``eq`` -> RGB: the bridge variant plus three per-plane 8-bit LUTs."""
|
|
324
|
+
|
|
325
|
+
link: BridgeLink # the negotiated eq link (pix_fmt / colourspace / range)
|
|
326
|
+
y_lut: tuple[int, ...]
|
|
327
|
+
u_lut: tuple[int, ...]
|
|
328
|
+
v_lut: tuple[int, ...]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@dataclass(frozen=True)
|
|
332
|
+
class LutStage:
|
|
333
|
+
"""Per-channel 8-bit LUT on straight RGB (``colorlevels``, ``curves``); alpha copied."""
|
|
334
|
+
|
|
335
|
+
r_lut: tuple[int, ...]
|
|
336
|
+
g_lut: tuple[int, ...]
|
|
337
|
+
b_lut: tuple[int, ...]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@dataclass(frozen=True)
|
|
341
|
+
class ColorBalanceStage:
|
|
342
|
+
"""``colorbalance`` option values as float32-rounded floats (rs gs bs rm gm bm rh gh bh)."""
|
|
343
|
+
|
|
344
|
+
shadows: tuple[float, float, float]
|
|
345
|
+
midtones: tuple[float, float, float]
|
|
346
|
+
highlights: tuple[float, float, float]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
Stage = YuvBridgeStage | LutStage | ColorBalanceStage
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@dataclass(frozen=True)
|
|
353
|
+
class ColorPipelinePayload:
|
|
354
|
+
filters: tuple[str, ...] # the exact CPU filter strings (diagnostics)
|
|
355
|
+
stages: tuple[Stage, ...]
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# --------------------------------------------------------------------------- lowering
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _split_options(handler_path: str, name: str, args: str) -> dict[str, str]:
|
|
362
|
+
"""``k=v:k=v`` -> dict; quoted values (``master='...'``) unquoted. Positional -> reject."""
|
|
363
|
+
|
|
364
|
+
options: dict[str, str] = {}
|
|
365
|
+
for item in _split_top_level(args):
|
|
366
|
+
if not item:
|
|
367
|
+
continue
|
|
368
|
+
if "=" not in item:
|
|
369
|
+
raise reject("effect (unsupported parameters)", f"{handler_path}: {name} positional option {item!r}")
|
|
370
|
+
key, value = item.split("=", 1)
|
|
371
|
+
options[key.strip()] = value.strip().strip("'\"")
|
|
372
|
+
return options
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _split_top_level(args: str) -> list[str]:
|
|
376
|
+
"""Split on ``:`` outside single quotes (``curves=master='0/0 0.2/0.21 ...'``)."""
|
|
377
|
+
|
|
378
|
+
parts: list[str] = []
|
|
379
|
+
current: list[str] = []
|
|
380
|
+
quoted = False
|
|
381
|
+
for char in args:
|
|
382
|
+
if char == "'":
|
|
383
|
+
quoted = not quoted
|
|
384
|
+
if char == ":" and not quoted:
|
|
385
|
+
parts.append("".join(current))
|
|
386
|
+
current = []
|
|
387
|
+
else:
|
|
388
|
+
current.append(char)
|
|
389
|
+
parts.append("".join(current))
|
|
390
|
+
return parts
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _option(options: dict[str, str], key: str, default: float) -> float:
|
|
394
|
+
return float(options.pop(key)) if key in options else default
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _reject_leftovers(handler_path: str, name: str, options: dict[str, str]) -> None:
|
|
398
|
+
if options:
|
|
399
|
+
raise reject(
|
|
400
|
+
"effect (unsupported parameters)",
|
|
401
|
+
f"{handler_path}: {name} option(s) {sorted(options)} are not ported",
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _clipf(value: float, low: float, high: float) -> float:
|
|
406
|
+
return max(low, min(high, value))
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _clipf32(value: float, low: float, high: float) -> float:
|
|
410
|
+
"""``av_clipf`` semantics: the double is rounded to float32 (the parameter type) and clipped."""
|
|
411
|
+
|
|
412
|
+
return _clipf(float(np.float32(value)), low, high)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# ---- eq ------------------------------------------------------------------------------------
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def eq_process_lut(contrast: float, brightness: float) -> tuple[int, ...]:
|
|
419
|
+
"""``process_c`` (vf_eq.h) as a LUT: ``((src * c) >> 12) + b`` clipped.
|
|
420
|
+
|
|
421
|
+
``vf_eq.c`` stores every control through ``av_clipf`` (a *float* function), so the LUT is
|
|
422
|
+
built from the float32-rounded values promoted back to double: ``brightness=-0.28`` makes
|
|
423
|
+
``100*b + 100`` = 71.9999998... -> ``(int)`` 71 (Y-75), where a plain double gives 72 (Y-73).
|
|
424
|
+
Callers that already went through ``_clipf32`` are unaffected (rounding is idempotent);
|
|
425
|
+
direct callers (``fx_branched`` Callout constants) rely on it. Divisions use C truncation
|
|
426
|
+
(``c`` is negative for negative contrast).
|
|
427
|
+
|
|
428
|
+
Main callers: ``_eq_stage`` (E4 eq bridge), ``fx_branched`` (Callout field / chroma LUTs).
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
contrast = float(np.float32(contrast))
|
|
432
|
+
brightness = float(np.float32(brightness))
|
|
433
|
+
c = int(contrast * 256 * 16)
|
|
434
|
+
b = _c_div(int(100.0 * brightness + 100.0) * 511, 200) - 128 - _c_div(c, 32)
|
|
435
|
+
return tuple(min(255, max(0, ((src * c) >> 12) + b)) for src in range(256))
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _eq_gamma_lut(contrast: float, brightness: float, gamma: float, gamma_weight: float = 1.0) -> tuple[int, ...]:
|
|
439
|
+
"""``create_lut`` (vf_eq.c): double math, ``(uint8_t)(256.0 * v)`` truncation."""
|
|
440
|
+
|
|
441
|
+
g = 1.0 / gamma
|
|
442
|
+
lw = 1.0 - gamma_weight
|
|
443
|
+
lut = []
|
|
444
|
+
for i in range(256):
|
|
445
|
+
v = i / 255.0
|
|
446
|
+
v = contrast * (v - 0.5) + 0.5 + brightness
|
|
447
|
+
if v <= 0.0:
|
|
448
|
+
lut.append(0)
|
|
449
|
+
continue
|
|
450
|
+
v = v * lw + math.pow(v, g) * gamma_weight
|
|
451
|
+
lut.append(255 if v >= 1.0 else int(256.0 * v))
|
|
452
|
+
return tuple(lut)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
_IDENTITY_LUT: Final = tuple(range(256))
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _eq_stage(handler_path: str, options: dict[str, str], link: BridgeLink) -> YuvBridgeStage:
|
|
459
|
+
"""``eq=...`` -> per-plane LUTs with vf_eq.c's ``check_values`` dispatch (option clamps included)."""
|
|
460
|
+
|
|
461
|
+
# ``av_clipf`` takes *float* arguments: every eq control is rounded to float32 on its way
|
|
462
|
+
# into the double fields (``brightness=-0.4`` really is -0.4000000059604645, which is what
|
|
463
|
+
# makes ``(int)(100*b + 100)`` = 59, not 60).
|
|
464
|
+
contrast = _clipf32(_option(options, "contrast", 1.0), -1000.0, 1000.0)
|
|
465
|
+
brightness = _clipf32(_option(options, "brightness", 0.0), -1.0, 1.0)
|
|
466
|
+
saturation = _clipf32(_option(options, "saturation", 1.0), 0.0, 3.0)
|
|
467
|
+
gamma = _clipf32(_option(options, "gamma", 1.0), 0.1, 10.0)
|
|
468
|
+
_reject_leftovers(handler_path, "eq", options) # gamma_r/g/b/weight/eval are never emitted
|
|
469
|
+
# Luma: param[0] = (contrast, brightness, gamma * gamma_g=1).
|
|
470
|
+
if contrast == 1.0 and brightness == 0.0 and gamma == 1.0:
|
|
471
|
+
y_lut = _IDENTITY_LUT
|
|
472
|
+
elif gamma == 1.0 and abs(contrast) < 7.9:
|
|
473
|
+
y_lut = eq_process_lut(contrast, brightness)
|
|
474
|
+
else:
|
|
475
|
+
y_lut = _eq_gamma_lut(contrast, brightness, gamma)
|
|
476
|
+
# Chroma: param[1..2] = (saturation, 0, sqrt(gamma_b/gamma_g) = 1.0).
|
|
477
|
+
if saturation == 1.0:
|
|
478
|
+
chroma_lut = _IDENTITY_LUT
|
|
479
|
+
elif abs(saturation) < 7.9:
|
|
480
|
+
chroma_lut = eq_process_lut(saturation, 0.0)
|
|
481
|
+
else: # unreachable (saturation is clipped to 3.0) but keeps the C dispatch complete
|
|
482
|
+
chroma_lut = _eq_gamma_lut(saturation, 0.0, 1.0)
|
|
483
|
+
return YuvBridgeStage(link=link, y_lut=y_lut, u_lut=chroma_lut, v_lut=chroma_lut)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# ---- colorlevels ---------------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _colorlevels_lut(in_min: float, in_max: float, out_min: float, out_max: float) -> tuple[int, ...]:
|
|
490
|
+
"""8-bit ``colorlevels`` channel LUT: ``clip((int)(float32(src - imin) * coeff + omin))``."""
|
|
491
|
+
|
|
492
|
+
imin = int(round(in_min * 255)) # lrint (half-even; the emitted values never sit on .5)
|
|
493
|
+
imax = int(round(in_max * 255))
|
|
494
|
+
omin = int(round(out_min * 255))
|
|
495
|
+
omax = int(round(out_max * 255))
|
|
496
|
+
if imin < 0 or imax < 0:
|
|
497
|
+
raise ValueError("colorlevels auto (negative) input levels are not emitted by the reference")
|
|
498
|
+
coeff = np.float32((omax - omin) / float(imax - imin))
|
|
499
|
+
values = np.float32(np.arange(256) - imin) * coeff + np.float32(omin)
|
|
500
|
+
return tuple(int(v) for v in np.clip(np.trunc(values.astype(np.float64)), 0, 255))
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _colorlevels_stage(handler_path: str, options: dict[str, str]) -> LutStage:
|
|
504
|
+
luts = []
|
|
505
|
+
for channel in "rgb":
|
|
506
|
+
luts.append(_colorlevels_lut(
|
|
507
|
+
_option(options, f"{channel}imin", 0.0), _option(options, f"{channel}imax", 1.0),
|
|
508
|
+
_option(options, f"{channel}omin", 0.0), _option(options, f"{channel}omax", 1.0),
|
|
509
|
+
))
|
|
510
|
+
# Alpha levels default to identity (aimin=0 aimax=1 aomin=0 aomax=1); the emitter never sets them.
|
|
511
|
+
for key in ("aimin", "aimax", "aomin", "aomax", "preserve"):
|
|
512
|
+
if key in options:
|
|
513
|
+
raise reject("effect (unsupported parameters)", f"{handler_path}: colorlevels {key} is not ported")
|
|
514
|
+
_reject_leftovers(handler_path, "colorlevels", options)
|
|
515
|
+
return LutStage(*luts)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# ---- colorbalance --------------------------------------------------------------------------
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _colorbalance_stage(handler_path: str, options: dict[str, str]) -> ColorBalanceStage:
|
|
522
|
+
def zone(suffix: str) -> tuple[float, float, float]:
|
|
523
|
+
return tuple( # type: ignore[return-value]
|
|
524
|
+
float(np.float32(_clipf(_option(options, f"{channel}{suffix}", 0.0), -1.0, 1.0)))
|
|
525
|
+
for channel in "rgb"
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
shadows, midtones, highlights = zone("s"), zone("m"), zone("h")
|
|
529
|
+
if _option(options, "pl", 0.0) != 0.0:
|
|
530
|
+
raise reject("effect (unsupported parameters)", f"{handler_path}: colorbalance pl=1 (preserve lightness) is not ported")
|
|
531
|
+
_reject_leftovers(handler_path, "colorbalance", options)
|
|
532
|
+
return ColorBalanceStage(shadows=shadows, midtones=midtones, highlights=highlights)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# ---- curves --------------------------------------------------------------------------------
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _curves_lut(points: Sequence[tuple[float, float]]) -> tuple[int, ...]:
|
|
539
|
+
"""``interpolate()`` (vf_curves.c, natural cubic spline, 8-bit): the master LUT."""
|
|
540
|
+
|
|
541
|
+
scale = 255
|
|
542
|
+
n = len(points)
|
|
543
|
+
if n == 0:
|
|
544
|
+
return _IDENTITY_LUT
|
|
545
|
+
if n == 1:
|
|
546
|
+
return tuple([min(255, max(0, int(points[0][1] * scale)))] * 256)
|
|
547
|
+
xs = [p[0] for p in points]
|
|
548
|
+
ys = [p[1] for p in points]
|
|
549
|
+
h = [xs[i + 1] - xs[i] for i in range(n - 1)]
|
|
550
|
+
r = [0.0] * n
|
|
551
|
+
for i in range(1, n - 1):
|
|
552
|
+
r[i] = 6 * ((ys[i + 1] - ys[i]) / h[i] - (ys[i] - ys[i - 1]) / h[i - 1])
|
|
553
|
+
bd = [0.0] * n
|
|
554
|
+
md = [0.0] * n
|
|
555
|
+
ad = [0.0] * n
|
|
556
|
+
md[0] = md[n - 1] = 1.0
|
|
557
|
+
for i in range(1, n - 1):
|
|
558
|
+
bd[i] = h[i - 1]
|
|
559
|
+
md[i] = 2 * (h[i - 1] + h[i])
|
|
560
|
+
ad[i] = h[i]
|
|
561
|
+
for i in range(1, n):
|
|
562
|
+
den = md[i] - bd[i] * ad[i - 1]
|
|
563
|
+
k = 1.0 / den if den else 1.0
|
|
564
|
+
ad[i] *= k
|
|
565
|
+
r[i] = (r[i] - bd[i] * r[i - 1]) * k
|
|
566
|
+
for i in range(n - 2, -1, -1):
|
|
567
|
+
r[i] = r[i] - ad[i] * r[i + 1]
|
|
568
|
+
lut = [0] * 256
|
|
569
|
+
|
|
570
|
+
def clip8(value: float) -> int:
|
|
571
|
+
return min(255, max(0, int(value))) # av_clip_uint8((int) v): C truncation
|
|
572
|
+
|
|
573
|
+
for i in range(int(xs[0] * scale)):
|
|
574
|
+
lut[i] = clip8(ys[0] * scale)
|
|
575
|
+
for i in range(n - 1):
|
|
576
|
+
yc, yn = ys[i], ys[i + 1]
|
|
577
|
+
a = yc
|
|
578
|
+
b = (yn - yc) / h[i] - h[i] * r[i] / 2.0 - h[i] * (r[i + 1] - r[i]) / 6.0
|
|
579
|
+
c = r[i] / 2.0
|
|
580
|
+
d = (r[i + 1] - r[i]) / (6.0 * h[i])
|
|
581
|
+
x_start = int(xs[i] * scale)
|
|
582
|
+
x_end = int(xs[i + 1] * scale)
|
|
583
|
+
for x in range(x_start, x_end + 1):
|
|
584
|
+
xx = (x - x_start) * 1.0 / scale
|
|
585
|
+
yy = a + b * xx + c * xx * xx + d * xx * xx * xx
|
|
586
|
+
lut[x] = clip8(yy * scale)
|
|
587
|
+
for i in range(int(xs[-1] * scale), 256):
|
|
588
|
+
lut[i] = clip8(ys[-1] * scale)
|
|
589
|
+
return tuple(lut)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _curves_stage(handler_path: str, options: dict[str, str]) -> LutStage:
|
|
593
|
+
master = options.pop("master", options.pop("m", None))
|
|
594
|
+
if master is None:
|
|
595
|
+
raise reject("effect (unsupported parameters)", f"{handler_path}: curves without a master curve")
|
|
596
|
+
_reject_leftovers(handler_path, "curves", options) # r/g/b/all/preset/psfile/interp never emitted
|
|
597
|
+
points: list[tuple[float, float]] = []
|
|
598
|
+
for token in master.split():
|
|
599
|
+
x_text, y_text = token.split("/")
|
|
600
|
+
x, y = float(x_text), float(y_text)
|
|
601
|
+
if not (0.0 <= x <= 1.0 and 0.0 <= y <= 1.0):
|
|
602
|
+
raise reject("effect (unsupported parameters)", f"{handler_path}: curves point {token!r} outside [0, 1]")
|
|
603
|
+
if points and int(points[-1][0] * 255) >= int(x * 255):
|
|
604
|
+
raise reject("effect (unsupported parameters)", f"{handler_path}: curves points not strictly increasing ({token!r})")
|
|
605
|
+
points.append((x, y))
|
|
606
|
+
lut = _curves_lut(points)
|
|
607
|
+
return LutStage(lut, lut, lut) # component curves are identity, composed with master
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
_STAGE_PARSERS: Final = {
|
|
611
|
+
"colorlevels": _colorlevels_stage,
|
|
612
|
+
"colorbalance": _colorbalance_stage,
|
|
613
|
+
"curves": _curves_stage,
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _parse_stage(handler_path: str, filter_string: str, link: BridgeLink | None) -> Stage:
|
|
618
|
+
name, _, args = filter_string.partition("=")
|
|
619
|
+
if name == "eq":
|
|
620
|
+
if link is None:
|
|
621
|
+
raise reject(
|
|
622
|
+
"effect (unsupported parameters)",
|
|
623
|
+
f"{handler_path}: the plan did not resolve which pixel link the reference feeds "
|
|
624
|
+
"this effect stack (LowerContext.reference_effect_link is unset; see "
|
|
625
|
+
"fx_color.reference_effect_link)",
|
|
626
|
+
)
|
|
627
|
+
return _eq_stage(handler_path, _split_options(handler_path, name, args), link)
|
|
628
|
+
parser = _STAGE_PARSERS.get(name)
|
|
629
|
+
if parser is None:
|
|
630
|
+
raise reject("effect (unsupported parameters)", f"{handler_path}: reference filter {name!r} is not ported")
|
|
631
|
+
return parser(handler_path, _split_options(handler_path, name, args))
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def reference_effect_link(
|
|
635
|
+
*,
|
|
636
|
+
source_color_space: str | None,
|
|
637
|
+
source_color_range: str | None,
|
|
638
|
+
source_width: int,
|
|
639
|
+
source_height: int,
|
|
640
|
+
canvas_width: int,
|
|
641
|
+
canvas_height: int,
|
|
642
|
+
conform: str,
|
|
643
|
+
crop_mode: str | None,
|
|
644
|
+
folded_group_effect: bool = False,
|
|
645
|
+
) -> str:
|
|
646
|
+
"""Which pixel link the reference graph feeds an effect stack through (``<fmt>:<csp>:<range>``).
|
|
647
|
+
|
|
648
|
+
Read from ``ffmpeg._video_chain`` / ``_geometry_stage_filters`` / ``GeometryPlan._conform_filters``
|
|
649
|
+
and verified with ``ffmpeg -v verbose`` on real reference graphs (see the module doc):
|
|
650
|
+
|
|
651
|
+
* pixel format + range: the effect stack follows the pre-effect geometry stages, which
|
|
652
|
+
contain a ``perspective`` (always preceded by ``setparams=range=full`` and negotiated in
|
|
653
|
+
8-bit planar ``gbrap``) exactly when the conform resamples (Fit / Fill with a source size
|
|
654
|
+
different from the canvas), the crop is a camera crop, or the crop is an animated pan
|
|
655
|
+
(Ken Burns) -> ``gbrap`` / ``pc``. Otherwise the stack follows a bare ``format=rgba``
|
|
656
|
+
(+ ``crop`` / ``pad`` for trim / ``conform none``) -> ``rgba`` with the *source's* range
|
|
657
|
+
tag (``tv`` for tv / unknown, ``pc`` for full-range sources such as JPEG rasters). Static
|
|
658
|
+
transforms / corner pins live in the spatial *tail* (after effects). Folded *group*
|
|
659
|
+
effects run on the group surface after a bare ``format=rgba`` -> ``rgba`` / source range.
|
|
660
|
+
* colourspace: libavfilter propagates the source stream's colorspace tag to the YUV link
|
|
661
|
+
(``bt709`` for tagged camera media, unknown / ``gbr`` for PNG rasters -> the 601 default,
|
|
662
|
+
``bt470bg`` for JPEG); anything outside the two ported matrices rejects loudly.
|
|
663
|
+
|
|
664
|
+
Main callers:
|
|
665
|
+
- ``plan._effect_specs`` (requested wiring: passes the result as
|
|
666
|
+
``LowerContext.reference_effect_link``); tests.
|
|
667
|
+
"""
|
|
668
|
+
|
|
669
|
+
# Unported matrices are carried through by name (e.g. ``rgba:bt2020nc:tv``) and reject only
|
|
670
|
+
# when a chain actually needs the bridge (``_lower_color``), so a bt2020 source without
|
|
671
|
+
# colour effects never trips the plan.
|
|
672
|
+
csp_name = (source_color_space or "unknown").lower()
|
|
673
|
+
colorspace = LINK_COLORSPACE_ALIASES.get(csp_name, csp_name)
|
|
674
|
+
resamples = (not folded_group_effect) and (
|
|
675
|
+
crop_mode in ("crop", "pan")
|
|
676
|
+
or (conform in ("fit", "fill") and (source_width, source_height) != (canvas_width, canvas_height))
|
|
677
|
+
)
|
|
678
|
+
if resamples:
|
|
679
|
+
return str(BridgeLink("gbrap", colorspace, "pc"))
|
|
680
|
+
range_name = (source_color_range or "tv").lower()
|
|
681
|
+
color_range = "pc" if range_name in ("pc", "jpeg", "full") else "tv"
|
|
682
|
+
return str(BridgeLink("rgba", colorspace, color_range))
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _lower_color(effect: ResolvedEffect, ctx: LowerContext) -> ColorPipelinePayload:
|
|
686
|
+
"""Lower one Color Adjustments / Color Board effect from the CPU emitter's filter strings.
|
|
687
|
+
|
|
688
|
+
Everything about *what the authored controls mean* (key/name lookup, registry
|
|
689
|
+
``ffmpeg_scale`` / clamps / defaults, ``.12g`` formatting) stays in
|
|
690
|
+
``color.color_effect_filters`` -- the port only interprets the emitted ffmpeg filters, so
|
|
691
|
+
the two backends cannot drift apart on parameter semantics. Any emitted filter or option
|
|
692
|
+
the port does not implement is a loud ``effect (unsupported parameters)`` reject.
|
|
693
|
+
|
|
694
|
+
The ``eq`` bridge link comes from ``ctx.reference_effect_link`` (see
|
|
695
|
+
``reference_effect_link``). TODO(E4 -> core): ``effects.LowerContext`` does not carry that
|
|
696
|
+
field yet; until ``plan._effect_specs`` passes it, any chain that needs the bridge rejects
|
|
697
|
+
loudly rather than guess (the links differ by whole tone-curve pivots and matrices, not by
|
|
698
|
+
a rounding code). The requested diff is in the E4 report.
|
|
699
|
+
"""
|
|
700
|
+
|
|
701
|
+
handler_path = f"{ctx.clip_path}: {effect.name or effect.handler}"
|
|
702
|
+
filters = tuple(color_effect_filters(effect))
|
|
703
|
+
link_text = getattr(ctx, "reference_effect_link", None)
|
|
704
|
+
link: BridgeLink | None = None
|
|
705
|
+
if link_text is not None and any(item.startswith("eq=") for item in filters):
|
|
706
|
+
try:
|
|
707
|
+
link = BridgeLink.parse(link_text)
|
|
708
|
+
except ValueError as error:
|
|
709
|
+
raise reject("effect (unsupported parameters)", f"{handler_path}: {error}") from None
|
|
710
|
+
stages = tuple(_parse_stage(handler_path, item, link) for item in filters)
|
|
711
|
+
return ColorPipelinePayload(filters=filters, stages=stages)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
# --------------------------------------------------------------------------- apply
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
@lru_cache(maxsize=256)
|
|
718
|
+
def _lut_tensor(lut: tuple[int, ...], device: str) -> torch.Tensor:
|
|
719
|
+
return torch.tensor(lut, dtype=torch.int32, device=device)
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _floor_shift(values: torch.Tensor, bits: int) -> torch.Tensor:
|
|
723
|
+
"""Arithmetic ``>>`` on int32 tensors (floor division; MPS-safe)."""
|
|
724
|
+
|
|
725
|
+
return torch.div(values, 1 << bits, rounding_mode="floor")
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def rgba8_to_yuva444p8(rgba: torch.Tensor, link: BridgeLink) -> torch.Tensor:
|
|
729
|
+
"""swscale 8-bit RGB -> ``yuva444p`` on ``[4, H, W]`` int32 (RGBA plane order in; see the module doc).
|
|
730
|
+
|
|
731
|
+
``rgb32ToY_c`` / ``planar_rgb_to_y`` (same arithmetic) with ``rgb2yuv_table(link.colorspace)``,
|
|
732
|
+
``hScale16To15`` (x2, FFMIN 32767), then for a ``pc`` link ``lumRangeToJpeg_c`` /
|
|
733
|
+
``chrRangeToJpeg_c`` on the 15-bit intermediate, then ``yuv2plane1_8_c`` (``+64 >> 7``, no
|
|
734
|
+
dither). Alpha: ``rgba`` follows ``rgbaToA_c`` (+1 for A >= 128, saturating at 255),
|
|
735
|
+
``gbrap`` follows ``planar_rgb_to_a`` (exact). Bit-exact against ffmpeg 8.0.1 on every RGB
|
|
736
|
+
triple for all eight links.
|
|
737
|
+
"""
|
|
738
|
+
|
|
739
|
+
ry, gy, by, ru, gu, bu, rv, gv, bv = rgb2yuv_table(link.colorspace)
|
|
740
|
+
r, g, b, a = rgba[0], rgba[1], rgba[2], rgba[3]
|
|
741
|
+
|
|
742
|
+
def plane(cr: int, cg: int, cb: int, offset: int, to_jpeg: tuple[int, int]) -> torch.Tensor:
|
|
743
|
+
x14 = _floor_shift(cr * r + cg * g + cb * b + (offset << 14) + (1 << 8), 9)
|
|
744
|
+
x15 = torch.clamp(x14 * 2, max=32767)
|
|
745
|
+
if link.full_range:
|
|
746
|
+
coeff, shift_offset = to_jpeg
|
|
747
|
+
x15 = torch.clamp(_floor_shift(x15 * coeff + shift_offset, 14), max=32767)
|
|
748
|
+
return torch.clamp(_floor_shift(x15 + 64, 7), 0, 255)
|
|
749
|
+
|
|
750
|
+
y = plane(ry, gy, by, 32, _LUM_TO_JPEG)
|
|
751
|
+
u = plane(ru, gu, bu, 256, _CHR_TO_JPEG)
|
|
752
|
+
v = plane(rv, gv, bv, 256, _CHR_TO_JPEG)
|
|
753
|
+
alpha = a if link.pix_fmt == "gbrap" else torch.clamp(a + (a >= 128).to(torch.int32), max=255)
|
|
754
|
+
return torch.stack((y, u, v, alpha))
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def yuva444p8_to_rgba8(yuva: torch.Tensor, link: BridgeLink) -> torch.Tensor:
|
|
758
|
+
"""swscale ``yuva444p`` -> 8-bit RGB on ``[4, H, W]`` int32 (RGBA plane order out).
|
|
759
|
+
|
|
760
|
+
``yuv2rgb_write_full`` (``rgba``) / ``yuv2gbrp_full_X_c`` (``gbrap``) -- the same arithmetic --
|
|
761
|
+
with ``yuv2rgb_coefficients(link.colorspace, link.full_range)``. Bit-exact against ffmpeg
|
|
762
|
+
8.0.1 on every YUV triple for all eight links, including the 32-bit wrap of the B
|
|
763
|
+
accumulator in the limited-range sets (Y8=255 with U8 near 255 -> 0); alpha copied.
|
|
764
|
+
"""
|
|
765
|
+
|
|
766
|
+
k = yuv2rgb_coefficients(link.colorspace, link.full_range)
|
|
767
|
+
y, u, v, a = yuva[0], yuva[1], yuva[2], yuva[3]
|
|
768
|
+
t = (y - k.y_offset) * k.y_coeff + (1 << 21 >> 9)
|
|
769
|
+
du, dv = u - 128, v - 128
|
|
770
|
+
|
|
771
|
+
def channel(total: torch.Tensor) -> torch.Tensor:
|
|
772
|
+
# total is the C accumulator >> 9 (exact: every term is a multiple of 2**9).
|
|
773
|
+
wrapped_or_negative = (total < 0) | (total >= (1 << 22))
|
|
774
|
+
saturated = total >= (1 << 21)
|
|
775
|
+
return torch.where(wrapped_or_negative, torch.zeros_like(total),
|
|
776
|
+
torch.where(saturated, torch.full_like(total, 255), _floor_shift(total, 13)))
|
|
777
|
+
|
|
778
|
+
r = channel(t + dv * k.v2r)
|
|
779
|
+
g = channel(t + dv * k.v2g + du * k.u2g)
|
|
780
|
+
b = channel(t + du * k.u2b)
|
|
781
|
+
return torch.stack((r, g, b, a))
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _apply_lut(values: torch.Tensor, lut: tuple[int, ...]) -> torch.Tensor:
|
|
785
|
+
if lut is _IDENTITY_LUT:
|
|
786
|
+
return values
|
|
787
|
+
return _lut_tensor(lut, str(values.device))[values.long()]
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _apply_bridge(stage: YuvBridgeStage, rgba: torch.Tensor) -> torch.Tensor:
|
|
791
|
+
yuva = rgba8_to_yuva444p8(rgba, stage.link)
|
|
792
|
+
yuva = torch.stack((
|
|
793
|
+
_apply_lut(yuva[0], stage.y_lut), _apply_lut(yuva[1], stage.u_lut),
|
|
794
|
+
_apply_lut(yuva[2], stage.v_lut), yuva[3],
|
|
795
|
+
))
|
|
796
|
+
return yuva444p8_to_rgba8(yuva, stage.link)
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def _apply_luts(stage: LutStage, rgba: torch.Tensor) -> torch.Tensor:
|
|
800
|
+
return torch.stack((
|
|
801
|
+
_apply_lut(rgba[0], stage.r_lut), _apply_lut(rgba[1], stage.g_lut),
|
|
802
|
+
_apply_lut(rgba[2], stage.b_lut), rgba[3],
|
|
803
|
+
))
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _apply_colorbalance(stage: ColorBalanceStage, rgba: torch.Tensor) -> torch.Tensor:
|
|
807
|
+
"""``color_balance8`` (vf_colorbalance.c) in float32, ``pl=0``."""
|
|
808
|
+
|
|
809
|
+
x = rgba[:3].to(torch.float32) / 255.0
|
|
810
|
+
lightness = torch.amax(x, dim=0) + torch.amin(x, dim=0) # ``l`` in vf_colorbalance.c
|
|
811
|
+
a, b, scale = 4.0, 0.333, 0.7
|
|
812
|
+
w_s = torch.clamp((b - lightness) * a + 0.5, 0.0, 1.0) * scale
|
|
813
|
+
w_m = torch.clamp((lightness - b) * a + 0.5, 0.0, 1.0) * torch.clamp((1.0 - lightness - b) * a + 0.5, 0.0, 1.0) * scale
|
|
814
|
+
w_h = torch.clamp((lightness + b - 1) * a + 0.5, 0.0, 1.0) * scale
|
|
815
|
+
outputs = []
|
|
816
|
+
for index in range(3):
|
|
817
|
+
v = x[index]
|
|
818
|
+
v = v + stage.shadows[index] * w_s
|
|
819
|
+
v = v + stage.midtones[index] * w_m
|
|
820
|
+
v = v + stage.highlights[index] * w_h
|
|
821
|
+
v = torch.clamp(v, 0.0, 1.0)
|
|
822
|
+
outputs.append(torch.clamp(torch.round(v * 255.0), 0.0, 255.0).to(torch.int32))
|
|
823
|
+
return torch.stack((*outputs, rgba[3]))
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def apply_stages(stages: Sequence[Stage], rgba8: torch.Tensor) -> torch.Tensor:
|
|
827
|
+
"""Run the parsed reference stages on an int32 ``[4, H, W]`` straight-RGBA 0..255 frame."""
|
|
828
|
+
|
|
829
|
+
for stage in stages:
|
|
830
|
+
if isinstance(stage, YuvBridgeStage):
|
|
831
|
+
rgba8 = _apply_bridge(stage, rgba8)
|
|
832
|
+
elif isinstance(stage, LutStage):
|
|
833
|
+
rgba8 = _apply_luts(stage, rgba8)
|
|
834
|
+
elif isinstance(stage, ColorBalanceStage):
|
|
835
|
+
rgba8 = _apply_colorbalance(stage, rgba8)
|
|
836
|
+
else: # pragma: no cover - the payload is built by _parse_stage only
|
|
837
|
+
raise TypeError(f"unknown colour stage {stage!r}")
|
|
838
|
+
return rgba8
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _apply_color(payload: ColorPipelinePayload, canvas: torch.Tensor, ctx: ApplyContext) -> torch.Tensor:
|
|
842
|
+
if not payload.stages:
|
|
843
|
+
return canvas
|
|
844
|
+
code = premultiplied_to_code(canvas)
|
|
845
|
+
rgba8 = torch.clamp(torch.round(code), 0.0, 255.0).to(torch.int32)
|
|
846
|
+
rgba8 = apply_stages(payload.stages, rgba8)
|
|
847
|
+
return code_to_premultiplied(rgba8.to(canvas.dtype))
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
register(EffectPort(handler="color_adjustments", lower=_lower_color, apply=_apply_color))
|
|
851
|
+
register(EffectPort(handler="color_board", lower=_lower_color, apply=_apply_color))
|