0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

画像の指定色透過(クロマキー)をブラウザで実装する — RGBユークリッド距離のスポイト透過と境界をぼかす2段しきい値

0
Posted at

3行まとめ

  • 画像上の色をスポイトで選んで透過するブラウザ完結ツールを作った。AIは使わず、各ピクセルと選択色の RGBユークリッド距離 で透過するかを決めるだけ
  • 0〜50のスライダー1本を3次元の色距離に載せるため、閾値に √3 を掛けて「1チャンネルあたりの許容差」 に読み替える
  • 透明化のしきい値と、そこから先の ぼかし用しきい値の2段にして、境界を一気に切らず線形に半透明へフェードさせる

青や緑のスクリーンで撮った写真から被写体を抜く、ロゴの背景色を透過PNGにする——「特定の色だけ消したい」場面はデザイン作業でよくある。AI背景除去は被写体を賢く判定してくれるが、単色に近い背景なら、色の近さで機械的に消すクラシックな方法(クロマキー)の方が速くて制御しやすい。

ぱんだツールズの指定色透過ツールは、画像上をスポイトでクリックして透過したい色を選び、その色に近いピクセルを透明にするツール。処理はすべてブラウザ内の Canvas API で完結し、画像はサーバーに送信されない。

この記事では、色の近さをどう数値化するか、1本のスライダーを3次元の色空間の距離に対応させる √3 の話、境界を自然にぼかす2段しきい値、そしてスポイト用と処理用でキャンバスを分ける設計を、実装ベースで解説する。

色の近さ = RGBユークリッド距離

「選択した色に近いピクセルを消す」の「近い」を、RGBを3次元座標とみなしたユークリッド距離で測る。ピクセルの色 (r, g, b) と選択色 (target.r, target.g, target.b) の距離はこう。

const distance = Math.sqrt(
  (target.r - r) ** 2 + (target.g - g) ** 2 + (target.b - b) ** 2,
)

この距離が小さいほど選択色に近い。距離がしきい値より小さいピクセルのアルファ値(data[i + 3])を0にすれば、その色が透明になる。画像は getImageData で取り出した Uint8ClampedArray で、1ピクセル = RGBA の4要素なので、i += 4 で走査してアルファは i + 3 を触る。

for (let i = 0; i < data.length; i += 4) {
  const r = data[i], g = data[i + 1], b = data[i + 2]
  const distance = Math.sqrt((target.r - r) ** 2 + (target.g - g) ** 2 + (target.b - b) ** 2)
  // distance に応じて data[i + 3](アルファ)を書き換える
}

RGB距離は知覚的な色差とは厳密には一致しない(人間の目は緑に敏感、など)が、単色背景を消す用途では十分実用的で、計算も軽い。数百万ピクセルをループで回すので、重み付けの少ない素朴な距離が速度面でも効く。

スライダー1本を3次元距離に載せる — √3 の意味

UIの閾値スライダーは0〜50の1次元の値。一方、消すかどうかの判定は3次元(RGB)空間の距離で行う。この橋渡しに √3 を使う。

const thresholdDist = threshold * Math.sqrt(3)

理由は幾何。RGBの各チャンネルがちょうど t ずつ選択色とズレているピクセルの距離は、√(t² + t² + t²) = t√3 になる。つまり閾値に √3 を掛けておくと、スライダーの値 threshold は「各チャンネルあたり何段階までの差を同じ色とみなすか」という直感的な意味になる。threshold = 15 なら「RGB各成分が15前後ズレていても同じ背景色とみなす」に相当する。

RGB距離の最大値は 255 × √3 ≈ 441。スライダー最大の50でも 50√3 ≈ 87 と全体の約2割で、単色に近い背景を消すのにちょうどいいレンジになっている。1次元のUIを多次元の判定に載せるときは、こういう次元をまたぐ換算係数を意識するとスライダーの手触りが安定する。同じ発想は画像ピクセル差分の記事でも使っていて、あちらは「チャンネル合計」への換算だった。距離の測り方に合わせて係数が変わる。

境界をぼかす2段しきい値

背景を一律に「距離 < 閾値なら透明、それ以外は不透明」で切ると、被写体の輪郭がギザギザに立つ。そこでしきい値を2段にして、透明化ゾーンの外側に半透明のフェードゾーンを設ける。

const thresholdDist = threshold * Math.sqrt(3)
const blurDist = (threshold + blur) * Math.sqrt(3)

if (distance < thresholdDist) {
  data[i + 3] = 0                                    // 完全透明
} else if (distance < blurDist) {
  const ratio = (distance - thresholdDist) / (blurDist - thresholdDist)
  data[i + 3] = Math.round(ratio * 255)             // 線形に半透明へ
}
// distance >= blurDist は何もしない(元の不透明のまま)

3つの帯に分かれる。

  • distance < thresholdDist:選択色に十分近い → アルファ0で完全透明
  • thresholdDist ≤ distance < blurDist:境界付近 → 距離に応じてアルファを 0→255 に線形補間。近い側ほど透明、遠い側ほど不透明
  • distance ≥ blurDist:選択色から遠い(被写体)→ 何も触らず元のまま

ratio は透明化しきい値からぼかししきい値までの位置を 0〜1 で表し、それを255倍してアルファにする。これで背景と被写体の境目が、くっきり切れずになめらかに半透明へ移り変わる。ぼかし幅は blur スライダー(0〜10)で、距離換算では blur × √3 ぶんの帯になる。

ちなみに blur = 0 のときは blurDist === thresholdDist になり、中央の分岐(distance < blurDist)に入る条件が最初の分岐で先に消化されるため、フェードゾーンは幅ゼロ=ハードエッジになる。blurDist - thresholdDist の0除算にも到達しない。しきい値を2段に積むと、ぼかしのオン・オフを別フラグで持たずに幅の連続値ひとつで表現できる。

スポイトは縮小キャンバス、処理は原寸キャンバス

色の選択(スポイト)は、画像上をクリックしてその位置のピクセル色を拾う。ここでキャンバスを2枚使い分けているのが設計のポイント。

表示・スポイト用のキャンバスは、画面に収まるよう縮小して描く。

const scale = Math.min(1, maxW / img.naturalWidth)
canvas.width = img.naturalWidth * scale
canvas.height = img.naturalHeight * scale
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)

クリック時は、その1点だけ getImageData(x, y, 1, 1) で色を取り出す。画像全体を読む必要はなく、1ピクセルだけ拾えばいい。

const pixel = ctx.getImageData(x, y, 1, 1).data
const color = { r: pixel[0], g: pixel[1], b: pixel[2] }

一方、実際の透過処理は原寸のキャンバスで行う。処理時は別途 naturalWidth × naturalHeight のキャンバスを作り、そこに原寸で描いてから全ピクセルを走査する。

canvas.width = img.naturalWidth
canvas.height = img.naturalHeight
ctx.drawImage(img, 0, 0)
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const processed = removeTargetColor(imageData, targetColor, threshold, blurRange)
ctx.putImageData(processed, 0, 0)

「表示・色拾いは軽く縮小版で、出力は劣化させないよう原寸で」という住み分け。色の選択に必要なのはおおよその色であって画素の完全一致ではないので、スポイトを縮小版でやっても実害はなく、大きな画像でもプレビューが軽い。出力は putImageData で書き戻して toBlob(..., 'image/png') でPNG化する。透過にはアルファチャンネルが要るので出力はPNG一択(JPEGは透過を持てない)。プレビュー背景に市松模様をCSSグラデーションで敷いて、透過部分が目で見て分かるようにしているのも地味な工夫。

単色クロマキーの限界を正直に

この手法は基準が1色なので、得意・不得意がはっきりしている。

  • 得意:グリーンバック・単色スクリーン・べた塗りの背景色。距離で機械的に消せる
  • 不得意:グラデーション背景(1色で近似できない)。閾値を広げれば近い色までカバーできるが、被写体に背景色と近い部分があると一緒に消える

被写体を意味的に理解して切り抜くAI背景除去(ONNXでブラウザ完結の背景除去を実装した記事で扱った)とは守備範囲が違う。単色背景なら色距離の方が速くて予測しやすく、複雑な背景ならAIが要る。道具として使い分けるのが正解で、このツールは前者に振り切っている。

まとめ

  • 指定色透過は、各ピクセルと選択色の RGBユークリッド距離 で消すかを決める素朴なクロマキー。getImageDataUint8ClampedArrayi += 4 で走査しアルファ(i + 3)を書き換える
  • 0〜50のスライダーを3次元距離に載せるため、閾値に √3 を掛けて「1チャンネルあたりの許容差」に読み替える。1次元UI×多次元判定は次元をまたぐ換算係数を意識する
  • 透明化しきい値とぼかししきい値の 2段にすると、境界を一気に切らず線形フェードで自然にできる。ぼかし幅ゼロなら自動でハードエッジ
  • スポイト・表示は縮小キャンバス、透過処理は原寸キャンバス、と役割で分ける。出力は透過を保つためPNG固定
  • 単色背景に強く、グラデーションや複雑背景はAI背景除去の領分。色距離とAIは使い分ける

グリーンバックやべた塗り背景の切り抜きにどうぞ。画像はブラウザの外に出ない。

ぱんだツールズ では他にも 画像圧縮・リサイズ・回転・モザイク・テキスト追加・PDF処理など、ブラウザ完結で使える画像・ファイルツールを多数公開中。全部無料・登録不要・ファイルはサーバーに送られない。
https://sakutto-panda.com


この記事は Zenn にも同じ内容を投稿しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?