ColorRevive は、白黒写真に色を付けるツールです。DeOldify の ONNX モデルを onnxruntime-web でブラウザ内推論し、出力の色だけを使って元写真の輝度と合成しています。写真はどこにも送りません。
#🎯 モデルの出力をそのまま出すと輪郭がぼける
カラー化モデルは 256×256 で推論します。ここが実装上の最大の制約でした。
素直に「モデルの出力を元サイズに拡大して表示」すると、細部が全部 256×256 相当までぼけます。顔も、文字も、建物の輪郭も。色は付いていますが、写真としては劣化しています。
そこで発想を変えました。
色だけモデルからもらい、明るさは元写真から使う。
人間の目は輝度の解像度に敏感で、色の解像度には鈍感です(テレビ放送やJPEGが色差を間引いているのと同じ理屈)。だから色が低解像度でも、輝度が原寸なら写真は原寸に見えます。
#🎨 YCbCr で輝度と色差を分ける
実装は RGB を YCbCr に分解して、輝度 Y を元写真から、色差 Cb/Cr をモデル出力から取ります。
// 元写真から 輝度 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)ので、詰め替えが要ります。
export function canvasToInput256(src: HTMLCanvasElement): Float32ArrayR が 65536 個、次に G が 65536 個、最後に B ── という順に詰めます。出力も同じ並びで返ってくるので、読み出すときは out256[i]、out256[plane + i]、out256[2 * plane + i] とプレーンの先頭にオフセットを足してアクセスします。
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 プレーンここを取り違えると、色がおかしいだけで動いてしまうので気づきにくいバグになります。
#🧪 「色が付いたか」を数値で確かめる
見た目の確認だけだと、テストが書けません。そこで最大彩度を測る関数を用意しました。
/** 画像の最大彩度 (0..255)。 e2e で「色が付いたか」を測るのに使う。 */
export function maxSaturation(data: Uint8ClampedArray): number白黒写真は彩度がほぼ 0 で、カラー化後は明確に上がります。「処理して彩度が上がったか」を自動で確かめられるので、モデルの読み込みや前処理が壊れたときに気づけます。
画像処理は「動いているように見えて何もしていない」が起きやすい領域です。以前、別のサービスで出力が入力そのままになっていたことがあり、それ以降出力を数値で検査する関数を必ず用意するようにしています。
#😓 苦労したところ
モデルのダウンロードが重い。 初回は数十 MB を落とします。進捗を出し、一度読んだらブラウザにキャッシュされることを明記しました。ここを黙って待たせると、ほぼ全員が離脱します。
サンプルに実写を使う。 図形やイラストをサンプルにすると、カラー化されても「本当に効いているのか」が伝わりません。かといって実写を同梱するとサイズが増えます。SVG で写実的な陰影を持つ絵を描いて同梱し、それを白黒にしたものを初期表示にしました。
これは以前、別の写真変換ツールでサンプルが抽象的すぎて効果が伝わらなかった反省からです。変換系は、変換前後が一目で分かる素材でないと機能を説明できません。
古い写真の粒子。 スキャンした古写真はノイズが多く、そのまま入れると色がまだらになります。前処理で軽く平滑化する案もありましたが、輝度は元写真のまま使うという方針と衝突するのでやめました。ノイズも含めて元の質感、という割り切りです。
#🔭 今後の拡張
- 部分的に色を指定する(この服は青、という手がかりを与える)
- 複数モデルの切り替え(絵画向け / 人物向け)
- 変換前後のスライダー比較(現在は並べて表示)
- 解像度の高いモデルへの差し替え
#💡 このサービスから言えること
モデルの制約は、合成の工夫で回避できることがあります。 256×256 という制約は変えられませんが、「輝度は元、色はモデル」に分ければ原寸の写真が作れました。モデルを大きくするより、出力の使い方を変えるほうが効いた例です。
そして人間の知覚の性質(輝度に敏感、色に鈍感)は、画像処理で何度も使える知識でした。JPEG も動画コーデックも同じ前提に立っています。
[ ./next_action ]
読んだら、 ColorRevive を実際に動かす。
この開発ログは ColorRevive をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。