フィルタ言語「MFG」とAI

フィルタ言語「MFG」とAI

MFGxAI

みなさんMFGをご存じでしょうか。

MFG(Modern Filter-language for GPU)は、2025年6月に発表されたGPU上で動作する画像フィルタを記述するための新しい言語です。( https://modernfilterlanguageforgpu.org/

イラストにおけるフィルタとは、色味や明るさ、雰囲気、質感を、加えたり変えられる機能のことを指します。光を足したり、ノイズや質感を重ねたりすることで、「完成度を上げる」「世界観を強める」といった仕上げができます。偶発的な効果で新たな表現を創り上げることもでき、ペイントツールユーザーにとっては非常に人気の機能のため、ありとあらゆるフィルタの需要が存在します。MFGは、その効果を柔軟に作り上げることができる新しいフィルタ言語なのです。


<フィルタの例:モノクロフィルタ>

このように、モノクロのほか、ぼかしやモザイクなど様々なフィルタが存在します。


私自身このMFGに触れたとき、これは単なるフィルタ記述言語にとどまらず、イラスト表現そのものを拡張する大きな可能性があるのではないかと感じました。

しかしその一方で実際にMFGでフィルタコードを書くには、一定の学習が必要であることも事実で、構文や型の扱い、GPU処理を前提とした考え方は、プログラマであっても慣れが求められますし、フィルタを使いたい・作りたいイラストレーターにとっては、さらに高いハードルになっているという現実があります。

多くのイラストレーターは、「こうしたい」「こうなるといいな」「やってみたらこうなった」といった感覚を頼りにフィルタを使用しています。そのため、「少し柔らかく」「明るい雰囲気」「光がにじむ」「ランダムに」といった抽象的で感覚的な言葉が先にあり、コードを書いて実現するという発想とは、そもそも立ち位置が大きく異なっています。

そこで考えるのは、感覚や直接でフィルタを作れないだろうかということですが、解決策は?となると昨今の技術環境を鑑み、生成AIを活用するという発想に行き着くのは自然な流れでした。

MFGという言語そのものを生成AIに理解させることで、感覚的な言葉からフィルタコードを生成できる可能性を探ることは現時点で最も現実的なアプローチではないでしょうか。全く新しい言語をAIに学習させて実用的な成果(フィルタコード)を出すというのは前例もなさそうですし、非常に期待が高まります。

ということで、ごく一般的なGPT( 5.2 plus )にMFGの公式ドキュメント( https://github.com/karino2/MFG/blob/main/docs/ja/README.md )を理解させ、次にMFGStudio( https://modernfilterlanguageforgpu.org/download/ )に付属するサンプルコードを学習させるという試みを行ってみました。


GPTにサンプルコードを学習させた結果

 

↑なかなか自信たっぷり。GPTらしさがでています。


最初のフィルタ生成検証の題材として選んだのが、油絵風の効果が得られる、Kuwahara Filter( https://ja.wikipedia.org/wiki/Kuwahara_filter )です。

Kuwahara Filterは比較的よく知られた画像処理フィルタであり、公開されている解説や資料も多く存在しているため、検証の出発点として適していると考えました。

まずはWikipediaに掲載されている説明をGPTに読ませ、その内容をもとにMFGのコード生成を指示しています。

すると、一度の指示でMFGのコードが生成され、実際にフィルタとして動作する状態まで到達しました。


実際の結果(動画)

実際生成されたコードを実行する様子。(写真画像が油絵風に変化)

 


私は専門的な正しさを細かく検証できる立場ではありませんが、フィルタの実行結果を確認する限り、少なくとも初回のテストとしては十分な成果が得られたと感じています。

(生成されたコードの質や処理方法が正しいかどうかを誰が判断するのか、という点については、別の課題として残っています。)


GPTが出力した実際のコード(初生成)


@title "Kuwahara Filter"

@param_i32 R(SLIDER, label="Radius", min=1, max=10, init=2)

let inputEx = sampler<input_u8>(address=.ClampToEdge)

def result_u8 |x, y| {
  let cnt = f32((R + 1) * (R + 1))
  let wL = [0.0722, 0.7152, 0.2127]

  let q1 = rsum(0..<R+1, 0..<R+1) |rx, ry| {
    let col = to_ncolor(inputEx(x + rx, y + ry))
    let lum = dot(col.xyz, wL)
    [*col, lum, lum * lum]
  }

  let q2 = rsum(0..<R+1, 0..<R+1) |rx, ry| {
    let col = to_ncolor(inputEx(x - rx, y + ry))
    let lum = dot(col.xyz, wL)
    [*col, lum, lum * lum]
  }

  let q3 = rsum(0..<R+1, 0..<R+1) |rx, ry| {
    let col = to_ncolor(inputEx(x - rx, y - ry))
    let lum = dot(col.xyz, wL)
    [*col, lum, lum * lum]
  }

  let q4 = rsum(0..<R+1, 0..<R+1) |rx, ry| {
    let col = to_ncolor(inputEx(x + rx, y - ry))
    let lum = dot(col.xyz, wL)
    [*col, lum, lum * lum]
  }

  let [b1, g1, r1, a1, l1, l2_1] = q1
  let [b2, g2, r2, a2, l2, l2_2] = q2
  let [b3, g3, r3, a3, l3, l2_3] = q3
  let [b4, g4, r4, a4, l4, l2_4] = q4

  let m1 = l1 / cnt
  let m2 = l2 / cnt
  let m3 = l3 / cnt
  let m4 = l4 / cnt

  let v1 = l2_1 / cnt - m1 * m1
  let v2 = l2_2 / cnt - m2 * m2
  let v3 = l2_3 / cnt - m3 * m3
  let v4 = l2_4 / cnt - m4 * m4

  let is1 = (v1 <= v2) && (v1 <= v3) && (v1 <= v4)
  let is2 = (!is1) && (v2 <= v3) && (v2 <= v4)
  let is3 = (!is1) && (!is2) && (v3 <= v4)
  let is4 = (!is1) && (!is2) && (!is3)

  let sb = ifel(is1, b1, 0.0) + ifel(is2, b2, 0.0) + ifel(is3, b3, 0.0) + ifel(is4, b4, 0.0)
  let sg = ifel(is1, g1, 0.0) + ifel(is2, g2, 0.0) + ifel(is3, g3, 0.0) + ifel(is4, g4, 0.0)
  let sr = ifel(is1, r1, 0.0) + ifel(is2, r2, 0.0) + ifel(is3, r3, 0.0) + ifel(is4, r4, 0.0)
  let sa = ifel(is1, a1, 0.0) + ifel(is2, a2, 0.0) + ifel(is3, a3, 0.0) + ifel(is4, a4, 0.0)

  let out = [sb, sg, sr, sa] / cnt
  to_u8color(out)
}

初生成成功後のGPTからの提案

 


次に試したのが、より抽象的なイメージだけを伝えるフィルタ生成です。

ここでは「イラスト向けの、星空を描画するフィルタを書けますか?」といった言葉だけを使い、数式やアルゴリズム名は一切与えていません。
このとき生成されたコードは、最初の段階ではMFGエディタ上でエラーを出しましたが、処理の方向性自体はイメージに沿ったものでした。
このやり取りを通して、イメージベースの指示にも対応できるMFGの柔軟性を体感しています。

GPTへの指示が複雑になるにつれて、構文エラーに遭遇する回数も増えていきますが、MFGエディタが返したエラーメッセージをそのままGPTに投げ返し、修正を試みました。
するとGPTは、その内容をもとにコードを調整し、数回の修正を経てエラーを解消していきます。


<実際のエラーと修正>

「イラスト向けの、星空を描画するフィルタを書けますか?」

<初回のコード>


@title "Starfield_BROKEN"

@param_f32 DENSITY(SLIDER, label="Star Density", min=0.001, max=0.02, init=0.008)
@param_f32 INTENSITY(SLIDER, label="Brightness", min=0.2, max=3.0, init=1.5)
@param_f32 BIGSTAR_RATE(SLIDER, label="Big Star Chance", min=0.0, max=0.05, init=0.01)
@param_f32 NOISE(SLIDER, label="Noise Amount", min=0.0, max=0.5, init=0.1)

let wh = f32(input_u8.extent())

@bounds(input_u8.extent(0), input_u8.extent(1))
def star_core |x, y, fx| {
    rand()
}

def dust_layer |x, y| {
    NOISE * rand()
}

@bounds(input_u8.extent(0), input_u8.extent(1))
def big_star_layer |x, y| {
    let r = rand()
}

let helper = fn(a, b) {
    a + b
}

@bounds(input_u8.extent(0), input_u8.extent(1))
def result_u8 |x, y| {
    let fx = f32(x) / wh.x
    let fy = f32(y) / wh.y

    let s = star_core(x, y, fx)
    let d = dust_layer(x, y)
    let b = big_star_layer(x, y)

    let v  = clamp((s + d + b) * INTENSITY, 0.0, 1.0)
    let iv = u8(255.0 * v)

    u8[iv, iv, iv, 255]
}

<初回エラー>

ParseError:11:5: Dim of @bounds and ts args are different. @bounds dim: 2, ts arg dim: 3.

*エラーコードをGPTに張り付けて修正を試みてもらう。

<修正1回目:エラー>

ParseError:18:5: Missing @bounds for tensor ‘dust_layer’.

<修正2回目:エラー>

ParseError:30:14: Unexpected token in ParseTargetableExprWOE: FN.

<修正3回目:エラー解消>


@title "Starfield"

# 密度・明るさ・大星の頻度を調整
@param_f32 DENSITY(SLIDER, label="Star Density", min=0.001, max=0.02, init=0.008)
@param_f32 INTENSITY(SLIDER, label="Brightness", min=0.2, max=3.0, init=1.5)
@param_f32 BIGSTAR_RATE(SLIDER, label="Big Star Chance", min=0.0, max=0.05, init=0.01)
@param_f32 NOISE(SLIDER, label="Noise Amount", min=0.0, max=0.5, init=0.1)

let wh = f32(input_u8.extent())

@bounds(input_u8.extent(0), input_u8.extent(1))
def star_core |x, y| {
    ifel(rand() < DENSITY, 1.0, 0.0)
}

@bounds(input_u8.extent(0), input_u8.extent(1))
def dust_layer |x, y| {
    NOISE * rand()
}

@bounds(input_u8.extent(0), input_u8.extent(1))
def big_star_layer |x, y| {
    let fx = f32(x) / wh.x
    let fy = f32(y) / wh.y
    let rx = rand()
    let ry = rand()

    ifel(
        rand() < BIGSTAR_RATE,
        exp(-20.0 * distance([fx, fy], [rx, ry])),
        0.0
    )
}

def result_u8 |x, y| {
    let s = star_core(x, y)
    let d = dust_layer(x, y)
    let b = big_star_layer(x, y)

    let v = clamp((s + d + 2.0 * b) * INTENSITY, 0.0, 1.0)
    let iv = u8(255.0 * v)

    u8[iv, iv, iv, 255]
}

<実行結果>

 

<エラーが発生した原因を聞いてみた>

 

<成功後は、GPTがオプションを提案してきた。>

オプション付きのコードはエラーもなく1度で実行に成功。(実際のコード)


@title "Ultimate Starfield"

# Blend mode: 0=Add, 1=Screen
@param_i32 MODE(SLIDER, label="Blend Mode (0=Add,1=Screen)", min=0, max=1, init=1)

# General stars
@param_f32 STAR_DENSITY(SLIDER, label="Star Density", min=0.001, max=0.02, init=0.006)
@param_f32 STAR_INTENSITY(SLIDER, label="Star Brightness", min=0.2, max=3.0, init=1.2)
@param_f32 STAR_NOISE(SLIDER, label="Dust Noise", min=0.0, max=0.5, init=0.08)

# Color stars
@param_f32 COLOR_STARS(SLIDER, label="Color Star Ratio", min=0.0, max=1.0, init=0.4)

# Big stars & cross flare
@param_f32 BIGSTAR_RATE(SLIDER, label="Big Star Chance", min=0.0, max=0.05, init=0.01)
@param_f32 FLARE_INTENSITY(SLIDER, label="Flare Strength", min=0.0, max=1.0, init=0.4)

# Milky Way band
@param_f32 MILKYWAY(SLIDER, label="Milky Way Strength", min=0.0, max=2.0, init=0.8)
@param_f32 MW_ANGLE(SLIDER, label="Milky Way Angle", min=0.0, max=6.283, init=0.6)

# Shooting star
@param_f32 SHOOTING_RATE(SLIDER, label="Shooting Star Chance", min=0.0, max=0.01, init=0.001)
@param_f32 SHOOTING_LEN(SLIDER, label="Shooting Length", min=5.0, max=80.0, init=40.0)

# Layered stars
@param_f32 LAYER2_RATE(SLIDER, label="Layer2 Density Multiplier", min=0.0, max=3.0, init=0.8)
@param_f32 LAYER3_RATE(SLIDER, label="Layer3 Density Multiplier", min=0.0, max=3.0, init=0.4)

let wh = f32(input_u8.extent())

def result_u8 |x, y| {
	let fx = f32(x) / wh.x
	let fy = f32(y) / wh.y

	let r = rand()
	let r2 = rand()
	let r3 = rand()

	let density1 = STAR_DENSITY
	let density2 = STAR_DENSITY * LAYER2_RATE
	let density3 = STAR_DENSITY * LAYER3_RATE

	let s1 = ifel(r  < density1, 1.0, 0.0)
	let s2 = ifel(r2 < density2, 1.0, 0.0)
	let s3 = ifel(r3 < density3, 1.0, 0.0)

	let base_star = (s1 + 0.7 * s2 + 0.4 * s3)

	let color_rand = rand()
	let star_color = ifel(
		color_rand < COLOR_STARS,
		[0.6 + 0.4 * rand(), 0.6 + 0.4 * rand(), 1.0],
		[1.0, 1.0, 1.0]
	)

	let bstar = base_star * STAR_INTENSITY
	let star_rgb = clamp(star_color * bstar, 0.0, 1.0)

	let bigrand = rand()
	let big_star = ifel(
		bigrand < BIGSTAR_RATE,
		exp(-25.0 * distance([fx, fy], [rand(), rand()])),
		0.0
	)

	let flarex = FLARE_INTENSITY * exp(-1500.0 * abs(fx - r2))
	let flarey = FLARE_INTENSITY * exp(-1500.0 * abs(fy - r3))
	let flare = big_star * (flarex + flarey)

	let big_rgb = clamp([1.0, 1.0, 1.0] * (big_star + flare), 0.0, 1.0)

	let mwx = fx * cos(MW_ANGLE) - fy * sin(MW_ANGLE)
	let mw = MILKYWAY * exp(-20.0 * (mwx - 0.5) ^ 2)

	let dust = STAR_NOISE * rand()

	let shooting = ifel(
		rand() < SHOOTING_RATE,
		exp(-distance([fx, fy], [rand(), rand()]) * SHOOTING_LEN),
		0.0
	)

	let final_star = clamp(star_rgb + big_rgb + mw + dust + shooting, 0.0, 1.0)

	let col = to_ncolor(input_u8(x, y))
	let add_rgb = clamp(col.xyz + final_star, 0.0, 1.0)
	let screen_rgb = 1.0 - (1.0 - col.xyz) * (1.0 - final_star)

	let out_rgb = ifel(MODE == 0, add_rgb, screen_rgb)

	to_u8color([*out_rgb, col.w])
}

一連のやり取りから、GPTが単にコードを生成しているだけでなく、実行環境からのフィードバックを受け取りながら調整を行っていることが分かります。

この実験を通じて感じているのは、生成AIが感覚的な言葉をフィルタコードへ変換できる可能性だけではありません。MFGという言語そのものが、AIとの対話に非常によく対応できる構造を持っているのではないかという点です。

イラストレーターが持つイメージを言葉として伝え、AIがそれをMFGコードに落とし込み、実行する。その過程で対話を重ねながら修正していける柔軟さは、非常に魅力的な点だと感じています。この流れが成立すれば、フィルタ制作の入り口は大きく変わっていきます。

イラストレーターが頭の中に描いているイメージを、そのまま言葉として伝え、フィルタという形に落とし込めるようになると、AIは主役として創作を行うのではなく、そのイメージを実現するための補助的な役割を担い、人間の感覚や判断を支える存在になります。

この関係性は、イラスト表現におけるAIの使い方として、新たな一つの理想的な形になり得るのではないでしょうか。

今後は実際に、現役のイラストレーターのイメージを具体化するテストも行ってみたいと思います。

それではまた。