何でも開く
libmpv が積んであるので、MKV・WebM・VP9・AV1・MPEG-1/2 まで、 AVFoundation が開けない形式もそのまま再生します。
RustyWorks / Kuro — 技術解説
Kuro は macOS の動画プレイヤーですが、中身の大半はカラーコレクターです。 再生しながら映像を grade し、その結果が業務用ツールと同じ数字になるように作ってあります。 このページは、その「同じ数字になる」を何によって担保しているかの説明です。
映像の色が合わないとき、原因のほとんどは演算そのものではなく どの空間で演算したかです。同じ「明るさ +10」でも、リニアで足すのと ガンマの掛かった符号値に足すのとでは結果が違います。
Kuro はパイプライン全体を Rec.709 / Gamma 2.4(BT.1886) に統一しています。
デコードは pow(v, 2.4) で、素通しの 128 が 128 のまま出ます。
以前は sRGB の EOTF を使っていて、これは中間調を約 7 コード持ち上げていました —
誰も操作していないのに絵が変わる、という状態です。廃止しました。
その上で、各コントロールは DaVinci Resolve(Rec.709 / Gamma 2.4 タイムライン)に対して 1つずつスポットプローブとスコープで突き合わせ、実測でモデル化してあります。 推測した式ではありません。基準パッチでおおむね ±2 コードの一致です。
実測から出た形はどれも素直ではありません。ガンマは 0 中心表記を実効指数へ移す必要があり
(G≥0: 1+4G / G<0: 1/(1−4G))、コントラストは
e=(2−contrast)^-1.17、ピボットは 0.435。色相回転にいたっては
回転量が色相自身に依存していて、2次高調波でフィットしています
(ΔH(H) = −25.63 − 7.43·cos2H + 15.96·sin2H)。
合わせるとはこういう作業です。
ここまでは Kuro の中の話です。macOS には続きがあります。
CAMetalLayer の色空間を extendedLinearDisplayP3 に
している以上、コンポジタが最後に sRGB 形状の OETF を掛けます。
パイプラインを 2.4 で統一しても、素直に出力したのでは
画面に届くのは 2.4 ではありません。
そこで最終段は srgbEOTF(gamma24Encode(·)) を書き込みます。
コンポジタの sRGB OETF がこれを打ち消すので、
パネルに残るのは純粋な 2.4 のコードです。
2.4 をそのままパネルへ送る DaVinci のビューアと一致するのは、この一段があるからです。
「素通しの 128 が 128 で出る」の根拠も、実はここまで含めた話です。
設定で sRGB を選ぶと、リニアで書いてコンポジタに任せます。 Mac の色管理としてはこちらが正確で、そして Gamma 2.4 より暗く出ます(DaVinci と比べて シャドウが約 9 コード)。
ここは向きが分かりにくいところです。「2.2 のほうが明るいはずでは」と 思ったなら、その直感は正しく、それがそのまま理由です。 Gamma 2.4 の側は、2.4 で符号化したコードを ≒2.2 のパネルが解くので、 基準より明るく出ます。sRGB の側はリニアに戻してから渡すため、 BT.1886 が指定するとおりの光量 —— つまり暗いほうになります。 設定の名前は何を送っているかであって、何が掛かるかではありません。 差はシャドウで最も大きく、ハイライトではほとんど消えます。
そして Gamma 2.4 が DaVinci と一致するのは、色として正しいから ではありません。DaVinci のビューアも同じようにコードをパネルへ 直接渡すからで、パネルが何をしようと両者が同じ影響を受ける、というだけです。 どちらが正しいかではなく、何に合わせるかの選択。 リファレンスに合わせたいなら 2.4、Mac の他のアプリと揃えたいなら sRGB。
自動ホワイトバランスの古典的なやり方はグレーワールド — 画面全体の平均が 無彩色になるように直す、というものです。単純で、そして赤い壁や夕景で必ず外します。 画面の平均は光源の色ではないからです。
Kuro は違う道を取ります。画面を 16×16 = 256 のゾーンに分け、 各ゾーンの色度を計算し、黒体軌跡からどれだけ離れているか(Duv)で ゾーンごとに採否を決めます。照明はほぼ軌跡の上に乗り、色のついた物体は乗らない — この一点だけで、平均では区別できなかったものが分かれます。
| ゾーン | CCT | Duv | 判定 |
|---|---|---|---|
| 中立グレー | 6500K | +0.0031 | 採用 |
| 電球 | 4175K | +0.0075 | 採用 |
| 曇天 | 9125K | +0.0027 | 採用 |
| 蛍光灯 | 6375K | +0.0165 | 採用 |
| 赤い壁 | 2200K | −0.0257 | 除外 |
| 芝生 | 6025K | +0.0617 | 除外 |
| 青い看板 | 20000K | −0.0415 | 除外 |
計算の健全性は 1 行で確かめられます。R=G=B を入れて 6500K / Duv +0.0031 が 返ればよい。D65 が軌跡から +0.003 だけ緑側にあるのは既知の値なので、 これが出るなら実装は正しいという基準になります。
採用されたゾーンの意見がどれだけ揃っているかが、そのまま信頼度になります。 信頼度が低ければ補正は控えられます。手がかりが無いときに無理に決めないことも、 自動処理の仕事のうちです。
暗い映像を明るくする一番素朴な方法はゲイン、つまり全体に定数を掛けることです。 これには避けられない副作用があります。1.0 に掛けると 1.0 を超える — 白が飛びます。持ち上げれば持ち上げるほど、上端から情報が消えていきます。
Kuro は露出を冪として扱います。符号空間でガンマを動かすことは、
リニアで冪を取ることと同じで((L^(1/2.4))^e)^2.4 = L^e)、
0 は 0 のまま、1 は 1 のままで中間だけが動きます。端点が原理的に壊れません。
暗い画をゲインで持ち上げると、測定した 21 点のうち複数が 1.0 を超えて白飛びした。 同じ量をガンマで動かすと、1 点も飛ばなかった。
自動調整で難しいのは直すことより直さないことです。 雪景色を「明るすぎる」と判断して暗くしてしまえば、撮影者の意図を壊します。 実素材 1287 フレームで各機構がどれだけ働いたかを測りました。
| 機構 | プロ素材 807 | アマ素材 480 |
|---|---|---|
| 逆光と判定 | 2.6% | 2.7% |
| レターボックス検出 | 3.8% | 0% |
| 平坦(グラフィック) | 0.4% | 2.9% |
| 露出をほぼ触らない | 88.8% | 96.7% |
レターボックス検出が、レターボックスのあるプロ素材にだけ 3.8% 出て、 アマチュア素材で 0% なのが分かりやすい例です。どの機構も枠は小さいが、空振りしていません。
HDR 映像を普通のプレイヤーで開くと、たいてい眠い絵になります。 1000 nits を前提に作られた信号を 100 nits の画面へ素直に押し込むからです。
Kuro は video-params/gamma が PQ / HLG を返した時点で経路を切り替え、
輝度にだけショルダーを掛けます。RGB を個別に潰さず、
輝度の比 y/Y を三成分に等しく掛けるので、
明るさを畳んでも色相が動きません。
曲線は Reinhard のショルダー
y = s·x/(1+s·x) · (1+s)/s(s = 2.62)で、
入力は BT.2408 の基準白 203 nits で正規化した輝度
x = Y · 203/(ソースピーク)。ソースのピークがちょうど 1.0 に乗ります。
ここは当初 ITU-R BT.2390 の EETF でした。 実素材で二度試して、二度とも測定で落ちています。 EETF は 203 nits の拡散白を画面の 0.88 に置いてしまい、 絵の大半がその上の狭い範囲に詰まる — 眠さを直すはずの経路が眠さを作っていました。 2026-08-28 に削除しました。規格の名前がそのまま良い絵になるとは限りません。
ヘッドルームについても、期待と実測が違いました。
maximumPotentialExtendedDynamicRangeColorComponentValue は
手元の2台(INNOCN 32M2V / BenQ SW2700)とも
実測 1.00、EDR レイヤーを実際に画面へ出した状態でも 1.00 です。
使える余地が無いものを設計の前提には置けないので、
現行の曲線はヘッドルームを見ずに 1.0 へ写します。
ヘッドルームのある画面で本当にハイライトを SDR 白より上へ出すのは、別の設計として残しています。
グレーディングが中心ですが、プレイヤーとしての足回りも同じ基準で作ってあります。 以下は無料で使える範囲と、Kuro PRO で開く範囲の実際の線引きです。
libmpv が積んであるので、MKV・WebM・VP9・AV1・MPEG-1/2 まで、 AVFoundation が開けない形式もそのまま再生します。
ゾーン測光と軌跡ベースの AWB。シーンが変わるたびに測り直す 「シーン毎」も無料側です。
波形・ベクトルスコープ・ヒストグラムを Metal compute で描画。 画面上の一点の補正後 Rec.709 値も読めます。
音声トラック切替、A/V 同期のずれ補正、静かなセリフを持ち上げて 大音量を抑えるナイトモード。
露出・黒レベル・ガンマ・コントラスト・色温度/ティント・彩度。 マスターの7本は無料側です。実測モデルはそのまま使えます。
全体切替と、画面を縦に割って左右で見比べるスライダー。 補正が何をしたのかを、記憶ではなく同時に見て確かめられます。
カラーホイールとチャンネル別の Lift / Gamma / Gain、オフセット(ASC CDL)、 3-Way、コントラストのピボット、色相(HSL)、カーブ、トーンマップ。
白い場所、適正にしたい場所を直接指すと、そこを基準に合わせます。 自動が迷う画で効きます。
作ったグレードを .cube に。ここで作った色を、そのまま別の編集ツールへ 持っていけます。
ぼけの逆畳み込み。フレーム毎に FFT は回さず、sigma と K が変わったときだけ CPU で空間カーネルを作り直して畳み込みます。
エッジを保存する空間バイラテラルと、動きに追従する時間 NR。 空のグラデーションに出る縞にはデバンド。
MetalFX、Anime4K、Apple の低遅延 SR。mpv は等倍で描き、 Metal のパスで拡大します。
.cube をテトラヘドラル補間で適用。強度も可変です。
メディア再生の世界で広く使われている部品には、GPL のものが多くあります。 GPL のコンポーネントが 1 つでも入ると Mac App Store では配布できません。品質の問題ではなく、ライセンスの問題です。
Kuro は同梱する libmpv と FFmpeg を -Dgpl=false / --disable-gpl で
自前ビルドしています。GPL のフィルタが 1 つでも混ざれば配布できないので、
入場券として払っている制約です。Homebrew のビルドは使えません。
スコープは普通、再生ソフトではなく専用の監視ツールに付くものです。 再生しながら波形とベクトルスコープを見るという形は、 この制約を先に払ったうえで選んだものです。
RustyWorks / Kuro — How it works
Kuro is a macOS video player, but most of what is inside it is a color corrector. It grades the picture during playback, and it is built so the result lands on the same numbers a professional tool produces. This page explains what makes “the same numbers” true.
When colors do not match, the cause is usually not the arithmetic — it is which space the arithmetic happened in. “Brightness +10” means one thing added to linear light and another thing added to a gamma-encoded code value.
Kuro puts the whole pipeline in Rec.709 / Gamma 2.4 (BT.1886).
Decode is pow(v, 2.4), so a passthrough 128 comes out as 128.
It used to use the sRGB EOTF, whose toe lifted midtones by about 7 code values —
the picture changed with nobody touching a control. That is gone.
On top of that, every control was matched against DaVinci Resolve (Rec.709 / Gamma 2.4 timeline) one at a time, with a spot probe and scopes, and modelled from the measurements. These are not guessed formulas. Agreement on the reference patches is roughly ±2 code values.
None of the shapes that came out of the measurements are tidy. Gamma needs its 0-centered
value carried to an effective exponent (G≥0: 1+4G / G<0: 1/(1−4G)),
contrast is e=(2−contrast)^-1.17 with a pivot of 0.435, and hue rotation has a
rotation amount that depends on hue itself, fitted as a second harmonic
(ΔH(H) = −25.63 − 7.43·cos2H + 15.96·sin2H).
That is what matching actually consists of.
That was all inside Kuro. macOS has a postscript. As long as the
CAMetalLayer colorspace is extendedLinearDisplayP3,
the compositor applies an sRGB-shape OETF at the very end.
Unifying the pipeline on 2.4 is not enough: write the value out plainly and
what reaches the screen is not 2.4.
So the final stage writes srgbEOTF(gamma24Encode(·)). The
compositor's sRGB OETF undoes that, and what lands on the panel is
the pure 2.4-encoded code — which is why it matches DaVinci's
viewer, a viewer that sends 2.4 straight to the display. “A passthrough 128
comes out as 128” only holds because of this step.
Choosing sRGB in the settings writes linear and lets the compositor do its thing. That is the colorimetrically accurate answer for the Mac panel, and it comes out darker than Gamma 2.4 — about 9 code values in the shadows.
The direction of that trips people up. If your first thought was “surely 2.2 is the brighter one”, you are right, and that is exactly the reason. On the Gamma 2.4 path, a 2.4-encoded code is decoded by a panel that is closer to 2.2, so it comes out brighter than the reference. The sRGB path converts back to linear first, so what the panel emits is the light BT.1886 actually specifies — the darker one. The name of the setting says what is being sent, not what gets applied to it. The gap is largest in the shadows and all but disappears in the highlights.
And Gamma 2.4 matches DaVinci not because it is colorimetrically right. It matches because DaVinci's viewer hands codes to the panel the same way, so whatever the panel does, it does to both. This is not a question of which is correct, but of what you are matching — 2.4 to match a reference, sRGB to match everything else on the Mac.
The classic approach to auto white balance is gray world — correct so the average of the whole frame is neutral. It is simple, and it is reliably wrong on a red wall or a sunset, because the average of the frame is not the color of the light.
Kuro takes a different route. The frame is divided into 16×16 = 256 zones, each zone’s chromaticity is computed, and each zone is accepted or rejected on how far it sits from the blackbody locus (Duv). Illuminants land close to the locus and colored objects do not — that single fact separates what an average cannot.
| Zone | CCT | Duv | Verdict |
|---|---|---|---|
| Neutral gray | 6500K | +0.0031 | accept |
| Tungsten | 4175K | +0.0075 | accept |
| Overcast | 9125K | +0.0027 | accept |
| Fluorescent | 6375K | +0.0165 | accept |
| Red wall | 2200K | −0.0257 | reject |
| Grass | 6025K | +0.0617 | reject |
| Blue sign | 20000K | −0.0415 | reject |
The soundness of the math can be checked in one line. Feed it R=G=B and it must return 6500K / Duv +0.0031. D65 sitting +0.003 to the green side of the locus is a known value, so getting it back is a real check on the implementation.
How well the accepted zones agree with each other becomes the confidence. When confidence is low, the correction is held back. Not deciding when there is nothing to decide from is part of an automatic system’s job.
The obvious way to brighten a dark shot is gain — multiply everything by a constant. That has a side effect you cannot avoid: multiply 1.0 and you go past 1.0 — highlights clip. The harder you lift, the more of the top end is thrown away.
Kuro treats exposure as a power. Moving gamma in the encoded space is the
same as taking a power in linear (((L^(1/2.4))^e)^2.4 = L^e), so
0 stays 0 and 1 stays 1 and only the middle moves. The endpoints cannot
break by construction.
Lifting a dark frame with gain pushed several of 21 measured patches past 1.0 and clipped them. Moving the same amount with gamma clipped none of them.
The hard part of auto-adjust is not correcting — it is not correcting. Decide that a snow scene is “too bright” and darken it, and you have destroyed what the person shooting it intended. Across 1287 frames of real material, this is how often each mechanism actually fired.
| Mechanism | Pro material, 807 | Amateur material, 480 |
|---|---|---|
| Judged backlit | 2.6% | 2.7% |
| Letterbox detected | 3.8% | 0% |
| Flat (graphics) | 0.4% | 2.9% |
| Exposure left essentially alone | 88.8% | 96.7% |
Letterbox detection is the legible one: 3.8% on professional material, which has letterboxing, and 0% on amateur material, which does not. Every mechanism has a small envelope, and none of them are swinging at nothing.
Open HDR footage in an ordinary player and it usually looks washed out: a signal authored for 1000 nits is being pushed straight into a 100-nit screen.
Kuro switches paths the moment video-params/gamma reports PQ or HLG, and
applies a shoulder to luminance only. It does not crush R, G and B
separately — it multiplies all three by the same luminance ratio y/Y, so
folding the brightness does not move the hue.
The curve is a Reinhard shoulder,
y = s·x/(1+s·x) · (1+s)/s with s = 2.62, taking a luminance
normalised on the BT.2408 reference white of 203 nits,
x = Y · 203/(source peak). The source peak lands exactly on 1.0.
This used to be the ITU-R BT.2390 EETF. It was tried on real material twice and rejected by measurement both times. The EETF puts 203-nit diffuse white at 0.88 of the display, packing most of the picture into the narrow band above it — the path that exists to fix washed-out was producing it. It was deleted on 2026-08-28. A standard’s name does not by itself make a good picture.
Headroom turned out differently from expectation too.
maximumPotentialExtendedDynamicRangeColorComponentValue measures
1.00 on both displays here (INNOCN 32M2V and BenQ SW2700), and still 1.00
with a real EDR layer actually on screen. You cannot base a design on room that is not
there, so the current curve maps to 1.0 without consulting headroom at all. Actually
putting highlights above SDR white on a display that does have headroom is kept as a
separate design.
Much of what the media-playback world is built on is GPL. One GPL component anywhere in a bundle and it cannot ship on the Mac App Store. That is a licensing fact, not a quality one.
Kuro builds its own libmpv and FFmpeg with -Dgpl=false /
--disable-gpl. One GPL filter anywhere in the bundle and it cannot be
distributed, so this is the constraint paid as an entry ticket.
Homebrew’s builds cannot be used.
Scopes normally belong to a dedicated monitoring tool rather than to playback software. Watching a waveform and a vectorscope while the video plays is a shape that only becomes available once that constraint is paid up front.
Grading is the center of it, but the player underneath is built to the same standard. Below is the actual line between what is free and what Kuro PRO opens.
libmpv is bundled, so MKV, WebM, VP9, AV1 and MPEG-1/2 play as they are, including what AVFoundation will not open.
Zone metering and locus-based AWB. Re-metering on every scene change is on the free side too.
Waveform, vectorscope and histogram drawn in Metal compute. The corrected Rec.709 value of any single point on screen is readable.
Track switching, A/V sync offset, and a night mode that lifts quiet dialogue while holding loud passages down.
Exposure, black level, gamma, contrast, temperature / tint, saturation. The seven master controls are on the free side, measured models and all.
A whole-frame toggle, and a divider you drag across the picture. What the correction did, seen side by side rather than from memory.
Color wheels and per-channel Lift / Gamma / Gain, offset (ASC CDL), 3-Way, the contrast pivot, hue (HSL), curves, tone map.
Point at what should be white, or at what should be correctly exposed, and it matches to that. For the frames automation hesitates on.
Write the grade to .cube and carry the color straight into
another editor.
Deconvolution of blur. No per-frame FFT — the spatial kernel is rebuilt on the CPU only when sigma or K changes, then convolved.
Edge-preserving spatial bilateral and motion-following temporal NR. Debanding for the rings in sky gradients.
MetalFX, Anime4K, and Apple’s low-latency SR. mpv draws at native size and a Metal pass scales it.
.cube applied with tetrahedral interpolation, with variable
strength.