▍AI Dev Lab
ColorRevive

ColorRevive ができるまで — onnxruntime-web で白黒写真をカラー化

DeOldify の量子化 ONNX を onnxruntime-web (CDN side-load・単一スレッド WASM=COOP/COEP不要) で実行。 256x256 でモデル推論し、輝度は元写真・色だけ AI を YCbCr で再合成して輪郭を保つ設計記録。写真は端末内処理。

ColorRevive は、白黒写真に色を付けるツールです。DeOldify の ONNX モデルを onnxruntime-web でブラウザ内推論し、出力の色だけを使って元写真の輝度と合成しています。写真はどこにも送りません。

#🎯 モデルの出力をそのまま出すと輪郭がぼける

カラー化モデルは 256×256 で推論します。ここが実装上の最大の制約でした。

素直に「モデルの出力を元サイズに拡大して表示」すると、細部が全部 256×256 相当までぼけます。顔も、文字も、建物の輪郭も。色は付いていますが、写真としては劣化しています。

そこで発想を変えました。

色だけモデルからもらい、明るさは元写真から使う。

人間の目は輝度の解像度に敏感で、色の解像度には鈍感です(テレビ放送やJPEGが色差を間引いているのと同じ理屈)。だから色が低解像度でも、輝度が原寸なら写真は原寸に見えます。

#🎨 YCbCr で輝度と色差を分ける

実装は RGB を YCbCr に分解して、輝度 Y を元写真から、色差 Cb/Cr をモデル出力から取ります。

ts
// 元写真から 輝度 Y を計算

const yo = 0.299 * od[p*4] + 0.587 * od[p*4+1] + 0.114 * od[p*4+2]

// モデル出力(拡大済み)から 色差 Cb / Cr を計算

const cb = (128 - 0.168736*r - 0.331264*g + 0.5*b - 128) * strength
const cr = (128 + 0.5*r - 0.418688*g - 0.081312*b - 128) * strength

// 元の輝度 + モデルの色差 で RGB に戻す

result.data[p*4]     = clamp(yo + 1.402 * cr)
result.data[p*4 + 1] = clamp(yo - 0.344136*cb - 0.714136*cr)
result.data[p*4 + 2] = clamp(yo + 1.772*cb)

係数は ITU-R BT.601 の標準変換式です。- 128 してから * strength を掛けているのがポイントで、Cb/Cr は 128 が無彩色の中心なので、中心からのずれに係数を掛けることで彩度を調整できます。

strength = 0 なら完全な白黒、1 ならモデルの色そのまま。色の強さをスライダーにできるのは、この分解をしているおかげです。

この方式にした結果、細部は元写真のまま、色だけが乗るという状態になりました。輪郭が一切ぼけません。

#📦 前処理 ── 256×256 の Float32 プレーンに詰める

モデルへの入力は、256×256 の RGB をチャンネルごとに並べた形(CHW)です。Canvas の ImageData はピクセルごとに RGBA が並ぶ(HWC)ので、詰め替えが要ります。

ts
export function canvasToInput256(src: HTMLCanvasElement): Float32Array

R が 65536 個、次に G が 65536 個、最後に B ── という順に詰めます。出力も同じ並びで返ってくるので、読み出すときは out256[i]、out256[plane + i]、out256[2 * plane + i] とプレーンの先頭にオフセットを足してアクセスします。

ts
img.data[i * 4]     = clamp(out256[i])              // R プレーン

img.data[i * 4 + 1] = clamp(out256[plane + i])      // G プレーン

img.data[i * 4 + 2] = clamp(out256[2 * plane + i])  // B プレーン

ここを取り違えると、色がおかしいだけで動いてしまうので気づきにくいバグになります。

#🧪 「色が付いたか」を数値で確かめる

見た目の確認だけだと、テストが書けません。そこで最大彩度を測る関数を用意しました。

ts
/** 画像の最大彩度 (0..255)。 e2e で「色が付いたか」を測るのに使う。 */
export function maxSaturation(data: Uint8ClampedArray): number

白黒写真は彩度がほぼ 0 で、カラー化後は明確に上がります。「処理して彩度が上がったか」を自動で確かめられるので、モデルの読み込みや前処理が壊れたときに気づけます。

画像処理は「動いているように見えて何もしていない」が起きやすい領域です。以前、別のサービスで出力が入力そのままになっていたことがあり、それ以降出力を数値で検査する関数を必ず用意するようにしています。

#😓 苦労したところ

モデルのダウンロードが重い。 初回は数十 MB を落とします。進捗を出し、一度読んだらブラウザにキャッシュされることを明記しました。ここを黙って待たせると、ほぼ全員が離脱します。

サンプルに実写を使う。 図形やイラストをサンプルにすると、カラー化されても「本当に効いているのか」が伝わりません。かといって実写を同梱するとサイズが増えます。SVG で写実的な陰影を持つ絵を描いて同梱し、それを白黒にしたものを初期表示にしました。

これは以前、別の写真変換ツールでサンプルが抽象的すぎて効果が伝わらなかった反省からです。変換系は、変換前後が一目で分かる素材でないと機能を説明できません。

古い写真の粒子。 スキャンした古写真はノイズが多く、そのまま入れると色がまだらになります。前処理で軽く平滑化する案もありましたが、輝度は元写真のまま使うという方針と衝突するのでやめました。ノイズも含めて元の質感、という割り切りです。

#🔭 今後の拡張

  • 部分的に色を指定する(この服は青、という手がかりを与える)
  • 複数モデルの切り替え(絵画向け / 人物向け)
  • 変換前後のスライダー比較(現在は並べて表示)
  • 解像度の高いモデルへの差し替え

#💡 このサービスから言えること

モデルの制約は、合成の工夫で回避できることがあります。 256×256 という制約は変えられませんが、「輝度は元、色はモデル」に分ければ原寸の写真が作れました。モデルを大きくするより、出力の使い方を変えるほうが効いた例です。

そして人間の知覚の性質(輝度に敏感、色に鈍感)は、画像処理で何度も使える知識でした。JPEG も動画コーデックも同じ前提に立っています。

[ ./next_action ]

読んだら、 ColorRevive を実際に動かす。

この開発ログは ColorRevive をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ShadowTwin

立体パズルの別解を作る:同じ影と最少の積み木

ShadowTwinの開発記録。3方向の図から別々の最少配置を2つ探す仕組み、5問の独立全探索、スマホの横並び比較、保存・再編集と入力検査を解説します。実照明の計算や能力診断ではありません。

read log →
SlitCard

スリットアニメーションを作る:4コマの合成と透明原稿の設計

SlitCardの開発記録。4コマの帯を交互に合成し、すき間と黒帯で別のコマを見せる仕組みを解説。同縮尺の台紙・透明シートSVG、50mm確認線、手動プレビュー・初期非表示、再編集と実出力検査を整理します。

read log →
WindowLight

日当たりシミュレーションを作る:窓の光を床へ投影する幾何モデル

WindowLightの開発記録。太陽位置近似から窓の4隅を床へ投影し、凸多角形を部屋で切り取る設計を解説。国立天文台の16時刻との限定照合、大気差・低高度・実際の採光との違い、SVGと設定の再編集を整理します。

read log →
GridLights

電源構成の比較を作る:割合と実量を100個の電球で読み解く

電源構成比較GridLightsの開発記録。12か国の年次発電データを最大剰余法で100個の電球に変換し、割合とTWhを比較する設計を解説。欠損を0補完しない7分類、予想と実値、出典付きSVGと実データ検証を整理します。

read log →
SoundLoom

音のお絵かきを作る:実WAVから絵を測り直す設計

音のお絵かきの開発記録。描画を正弦波のPCMとWAVへ変換し、保存音声の周波数をGoertzel法で測って比較する仕組みを解説。音量・測定窓・量子化の限界と、編集・離脱時の古い結果を抑止する検証を整理します。

read log →
MazeLoom

宝探し迷路を作る:寄り道まで含めた最短ルートの探索

宝探し迷路の開発記録。通路を木として生成し、現在地と3つの宝の取得状態を組み合わせて最短ルートを探す仕組みを解説。問題コードの版固定、画面と解答付きPDFの一致、手数と印刷の検証を整理します。

read log →