← Photoshot · ガイドと開発記↗エディターを開く
Photoshot / Development03

無料画像編集ソフト開発記 第3回:レベル・ヒストグラムを既存コードにつなぐ

レベル補正は黒点・白点・ガンマの式だけでは完成しない。GIMPの文書で動作を確認し、AIと計算、ハンドル、ヒストグラムを実装して既存の調整レイヤーへ接続した。GitHub公開コードの三つのファイルから組み合わせ方を見る。

出典照合 · 2026年10月16日改訂

記事を読む ↓
補正前 · AI生成の例示用人物画像
補正前 · AI生成の例示用人物画像
実際のPhotoshot編集画面 · 2026年10月13日
実際のPhotoshot編集画面 · 2026年10月13日
BUILD LOG / 01

GIMPで動作を確認し、Photoshot側で実装

入力の黒点・白点、出力範囲、ガンマ、個別チャンネルの意味はGIMPのLevels文書で確認した。これは動作の参考資料であり、GIMPのC実装を移植したと確認できる記録はない。

計算はadjustments.ts、ハンドル座標はlevels-controls.ts、ヒストグラム集計はtone-analysis.tsへ分けて実装した。今回はこの三つのPhotoshotファイルをたどる。

BUILD LOG / 02

既存のLUT経路へレベル計算を入れる

式は入力範囲の正規化、逆ガンマ、出力範囲への変換の順に読む。ループ内のrgbは共通設定、channelは個別のR・G・B設定だ。

計算は既存の256要素LUT生成処理に入る。独立した別機能として描画するのではなく、調整レイヤーのマスク、不透明度、保存の流れへ接続した。

コード抜粋 · src/adjustments.ts · v0.16.0 · L521–528 ↗
if (t === 'levels')
  for (const c of ['rgb', channel])
    x =
      (v[c + 'OutBlack'] +
        (v[c + 'OutWhite'] - v[c + 'OutBlack']) *
          clamp((x * 255 - v[c + 'Black']) / (v[c + 'White'] - v[c + 'Black'])) **
            (1 / v[c + 'Gamma'])) /
      255;
BUILD LOG / 03

中央ハンドルも計算と一致させる

中央ハンドルは単なる幾何学的な中央ではなく、補正後に50%となる入力位置を表す。gammaPositionがガンマから位置へ、gammaAtPositionが対数を使って位置からガンマへ変換する。

この対を用意することで数値入力とドラッグが同じ調整値を共有できる。画素を変える式だけでなく、双方向の接続が必要だった。

コード抜粋 · src/levels-controls.ts · v0.16.0 · L2–13 ↗
// The middle handle marks the input that becomes 50% after inverse gamma.
export function gammaPosition(black: number, white: number, gamma: number) {
  return black + (white - black) * Math.pow(0.5, clamp(gamma, 0.1, 10));
}
export function gammaAtPosition(black: number, white: number, position: number) {
  const t = clamp(
    (position - black) / Math.max(1, white - black),
    Math.pow(0.5, 10),
    Math.pow(0.5, 0.1),
  );
  return Math.round(clamp(Math.log(t) / Math.log(0.5), 0.1, 10) * 100) / 100;
}
BUILD LOG / 04

ヒストグラムにも透明度のルールを入れる

透明な画素を黒として数えると分布を誤って表示する。tone-analysis.tsではアルファを0〜1の重みとして使い、完全に透明な画素を除外する。明るさの分布とR・G・Bの分布も別に集計する。

これはAIと書いた分析コードだ。補正対象とグラフが同じ文書を参照するように接続し、計算関数を実際に使える道具へ変えていった。

コード抜粋 · src/tone-analysis.ts · v0.16.0 · L22–28 ↗
const weight = data[i + 3] / 255;
if (!weight) continue;
bins[Math.round(0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2])] += weight;
channelBins.r[data[i]] += weight;
channelBins.g[data[i + 1]] += weight;
channelBins.b[data[i + 2]] += weight;
count += weight;
BUILD LOG / 05

画面と比較記録

写真例はRGB入力12 / 1.08 / 245、出力0 / 255。人物の元画像はAI生成の例示素材で、編集画面と補正結果は実際のものだ。

過去のPhotoshop比較チャートは入力18 / 1.12 / 235、出力0 / 255という別設定で、平均チャンネル差は約0.229、最大1だった。ハンドルと数値の整合性は別途テストした。

補正前 · AI生成の例示用人物画像
補正前 · AI生成の例示用人物画像
補正後 · 実際のPhotoshot出力
補正後 · 実際のPhotoshot出力
BUILD LOG / 06

三つのファイルを一つの機能へ

レベルはGIMPの文書で動作を理解し、既存のPhotoshotへ計算・操作・分析をつないだ結果だ。公開ファイルを見れば数式とUIの役割を分けて読める。

AIと書いたコードの価値は行数より接続の正確さにあった。次回はglfx.jsで確認したLUTの考え方と、別に実装したPhotoshotの補間処理を追う。

実装と参考資料

ガイドと開発記の一覧 ↗↗実際に編集する