[C#] Cloudflare Clef 圖片辨識實測判斷泰國佛牌法像

  • 13
  • 0

上一篇使用 Cloudflare Clef 測試 XSS 字串,主要是利用 Decision Model 的 noul 判斷是否具有 XSS 特徵,再把機率輸出

後來翻了一下官方文件,發現 Clef 不只能處理文字,也可以直接傳圖片進去,而且也是不用花到錢

我自己平常有接觸泰國佛牌,我就想到有沒有可能直接用 Clef 來判斷法相-必打、七龍佛、坤平、崇迪、師父自身這些不同法像

目前就不苛責他能夠判斷來源年分,只要法相能對,這就有搞頭,畢竟可以做圖片初步分類..

Cloudflare Clef 除了前一篇使用的 noul,還支援 choice,可以預先定義幾個選項,讓模型直接判斷最符合哪一個,並回傳每個選項的機率。

這次我先建立 16 個分類:

類別說明
必打佛Phra Pidta
七龍佛Phra Nak Prok
師父自身Monk Portrait
坤平Phra Khun Phaen
崇迪佛Phra Somdej
帕洛佛Phra Rod
女王佛Phra Nang Phaya
龍婆托Luang Pu Thuat
象神Ganesha
四面神Phra Phrom
招財女神Nang Kwak
拉胡Rahu
哈奴曼Hanuman
古曼童Kuman Thong
冠蘭、符管等非佛像類聖物
其他不符合分類或無法確認

其實這個分類表還不算完整,泰國佛牌的種類非常多,而且有些法像會因為不同師父、不同模具而出現很大的差異。

不過先拿 16 種測試就好,至少能觀察 Clef 對幾種常見造型的判斷結果。

這次使用的模型一樣是:

@cf/cloudflare/clef

官方文件:Cloudflare Clef

使用 .NET 10 Console,不用額外安裝 NuGet 套件。

專案底下建立 images 資料夾,把要測試的照片放進去,這次先使用五張 JPG

AmuletVision/
├── AmuletVision.csproj
├── Program.cs
└── images/
    ├── 七龍佛.jpg
    ├── 自身.jpg
    ├── 坤平.jpg
    ├── 崇迪.jpg
    └── 符管.jpg

照片名稱只是為了方便核對,程式不會把檔名傳給 Clef,避免模型直接從檔名猜答案。

如果之前已經有 Workers AI API Token,可以直接沿用。

還沒有的話,可以到:

Cloudflare Workers AI Dashboard

這次一樣使用 Account ID 與 API Token 呼叫 REST API,程式範例中的憑證都改成佔位文字,正式環境記得改用環境變數

這次程式沿用前面的 HttpClient 寫法,差別是先把 JPG 讀成 Base64,放進 Request 的 images 裡面,再使用 choice 問題判斷法像


C# Code : 

using System.Diagnostics;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

var accountId = "YOUR_ACCOUNT_ID";
var apiToken = "YOUR_API_TOKEN";

var endpoint = $"https://api.cloudflare.com/client/v4/accounts/{accountId}/ai/run/@cf/cloudflare/clef";
var imageFolder = Path.Combine(Directory.GetCurrentDirectory(), "images");

if (!Directory.Exists(imageFolder))
{
    Console.WriteLine($"找不到資料夾:{imageFolder}");
    return;
}

var imageFiles = Directory.GetFiles(imageFolder)
    .Where(x => new[] { ".jpg", ".jpeg" }
        .Contains(Path.GetExtension(x).ToLowerInvariant()))
    .OrderBy(x => x)
    .ToArray();

if (imageFiles.Length == 0)
{
    Console.WriteLine("沒有找到 JPG 圖片");
    return;
}

// 定義 16 種佛牌法像及其外觀特徵
var categories = new Dictionary<string, string>
{
    // 必打佛
    ["phra_pidta"] =
        "Phra Pidta, a seated figure covering the face or eyes " +
        "with its hands, sometimes with multiple arms.",

    // 七龍佛
    ["phra_nak_prok"] =
        "Phra Nak Prok, a seated Buddha protected by a " +
        "multi-headed Naga serpent hood, commonly seven heads.",

    // 師父自身
    ["monk_portrait"] =
        "A Thai monk or Ajarn portrait amulet, showing a monk " +
        "in robes with a recognizable human face.",

    // 坤平
    ["phra_khun_phaen"] =
        "Phra Khun Phaen, a traditional seated Buddha amulet, " +
        "often inside a decorative arched niche.",

    // 崇迪佛
    ["phra_somdej"] =
        "Phra Somdej, a seated Buddha on multiple stepped bases, " +
        "often on a rectangular tablet.",

    // 帕洛佛
    ["phra_rod"] =
        "Phra Rod, a small Buddha inside a pointed or leaf-shaped niche.",

    // 女王佛
    ["phra_nang_phaya"] =
        "Phra Nang Phaya, a seated Buddha in a triangular amulet.",

    // 龍婆托
    ["luang_pu_thuat"] =
        "Luang Pu Thuat, a portrait or statue depicting " +
        "the revered Thai monk Luang Pu Thuat.",

    // 象神
    ["ganesha"] =
        "Ganesha, a deity with an elephant head.",

    // 四面神
    ["phra_phrom"] =
        "Phra Phrom, the four-faced Brahma deity.",

    // 招財女神
    ["nang_kwak"] =
        "Nang Kwak, a seated female figure with a raised beckoning hand.",

    // 拉胡
    ["phra_rahul"] =
        "Rahu, often depicted swallowing the sun or moon.",

    // 哈奴曼
    ["hanuman"] =
        "Hanuman, a monkey-faced warrior deity.",

    // 古曼童
    ["kumantong"] =
        "Kuman Thong, a child-shaped spirit figure.",

    // 冠蘭/符管
    ["kruang_rang"] =
        "Kruang Rang, a non-figurative Thai sacred charm such as " +
        "a takrut tube, yantra, sacred cloth or inscribed object.",

    // 其他
    ["other"] =
        "Other amulet, unknown figure, unclear image or no reliable match."
};

// 分類代碼對應中文名稱
var names = new Dictionary<string, string>
{
    ["phra_pidta"] = "必打佛",
    ["phra_nak_prok"] = "七龍佛/娜迦護佛",
    ["monk_portrait"] = "師父自身",
    ["phra_khun_phaen"] = "坤平",
    ["phra_somdej"] = "崇迪佛",
    ["phra_rod"] = "帕洛佛",
    ["phra_nang_phaya"] = "女王佛",
    ["luang_pu_thuat"] = "龍婆托",
    ["ganesha"] = "象神",
    ["phra_phrom"] = "四面神",
    ["nang_kwak"] = "招財女神",
    ["phra_rahul"] = "拉胡",
    ["hanuman"] = "哈奴曼",
    ["kumantong"] = "古曼童",
    ["kruang_rang"] = "冠蘭/符管/非佛像聖物",
    ["other"] = "其他/無法確定"
};

using var client = new HttpClient
{
    Timeout = TimeSpan.FromSeconds(120)
};

client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", apiToken);

Console.WriteLine($"圖片數量:{imageFiles.Length}");
Console.WriteLine($"分類數量:{categories.Count}");

foreach (var imagePath in imageFiles)
{
    var fileName = Path.GetFileName(imagePath);

    Console.WriteLine();
    Console.WriteLine(new string('=', 65));
    Console.WriteLine($"Testing: {fileName}");

    var timer = Stopwatch.StartNew();

    try
    {
        var bytes = await File.ReadAllBytesAsync(imagePath);

        // 單張圖片超過 4 MiB 就跳過
        if (bytes.Length > 4 * 1024 * 1024)
        {
            Console.WriteLine("圖片超過 4 MiB,跳過");
            continue;
        }

        // 轉成 Base64,放進 Clef 的 images 欄位
        var imageData = "data:image/jpeg;base64," +
            Convert.ToBase64String(bytes);

        var request = new
        {
            model = "clef",

            state =
                "Identify the main visible figure or sacred object " +
                "on this Thai amulet. Use visual evidence only. " +
                "Do not guess the maker, temple, age or authenticity. " +
                "Use other when the image is unclear.",

            images = new[] { imageData },

            questions = new
            {
                amulet_type = new
                {
                    type = "choice",

                    instructions =
                        "Which category best matches the visible amulet? " +
                        "Focus on distinctive visual details. " +
                        "Do not identify an unknown monk as Luang Pu Thuat " +
                        "without clear visual evidence. " +
                        "Choose other if no category is a reliable match.",

                    criteria = categories
                }
            }
        };

        using var response = await client.PostAsJsonAsync(endpoint, request);
        var json = await response.Content.ReadAsStringAsync();

        timer.Stop();

        if (!response.IsSuccessStatusCode)
        {
            Console.WriteLine($"HTTP {(int)response.StatusCode}");
            Console.WriteLine(json);
            continue;
        }

        using var document = JsonDocument.Parse(json);

        var answer = document.RootElement
            .GetProperty("result")
            .GetProperty("answers")
            .GetProperty("amulet_type");

        // 取得 Clef 選擇的法像
        var selected = answer.GetProperty("choice").GetString() ?? "";

        // 取得每一種法像的機率
        var scores = answer.GetProperty("probabilities")
            .EnumerateObject()
            .Where(x => x.Value.ValueKind == JsonValueKind.Number)
            .ToDictionary(x => x.Name, x => x.Value.GetDouble());

        var probability = scores.TryGetValue(selected, out var p)
            ? p
            : 0;

        var categoryName = names.GetValueOrDefault(selected, selected);

        Console.WriteLine($"預測法像:{categoryName}");
        Console.WriteLine($"判斷機率:{probability:P2}");
        Console.WriteLine($"HTTP 耗時:{timer.ElapsedMilliseconds} ms");

        // 依機率排序,顯示前三名
        Console.WriteLine();
        Console.WriteLine("Top 3:");

        foreach (var item in scores.OrderByDescending(x => x.Value).Take(3))
        {
            var name = names.GetValueOrDefault(item.Key, item.Key);
            Console.WriteLine($"  {name,-26} {item.Value:P2}");
        }

        // 最高機率低於 65% 就標記人工複核
        var status = selected == "other" || probability < 0.65
            ? "Review"
            : "OK";

        Console.WriteLine($"Status:{status}");
    }
    catch (Exception ex)
    {
        timer.Stop();
        Console.WriteLine($"Error:{ex.Message}");
    }
}

Console.WriteLine();
Console.WriteLine(new string('=', 65));
Console.WriteLine($"完成,共處理 {imageFiles.Length} 張圖片。");

這次五張照片都成功送進 Clef,Console 的結果整理如下:

圖片預測法像機率HTTP 耗時狀態
七龍佛.jpg七龍佛79.74%3016 msOK
自身.jpg師父自身84.29%974 msOK
坤平.jpg坤平51.46%1345 msReview
崇迪.jpg坤平15.26%946 msReview
符管.jpg冠蘭/符管90.13%739 msOK

 

五張的平均 HTTP 耗時大約 1.4 秒,七龍佛那張最慢,超過 3 秒,但是竟然對了
 

七龍佛、師父自身和符管的分類都符合預期,而且機率大概在 80%~90% 左右

坤平也有判對,但是只有 51.46%,第二名崇迪佛還有 19.12%,第三名帕洛佛 10.51%,代表在這幾種相近的坐佛法像之間,模型並沒有非常確定

但是可能因為是神獸崇迪的關係她跟傳統崇迪還是有點差異,所以他改判成坤平,這我比較訝異,不過我覺得敘述的夠好應該分類就會正確
 

畢竟那邊我也是靠 AI 先幫我寫初稿

目前這五張如果按照檔名所代表的已知法像核對,算是五張裡面四張分類符合預期

不過這不是完整的模型準確率,因為樣本很少,而且只有測一輪

我覺得如果只是用來整理自己的佛牌照片,或是先自動幫圖庫產生候選分類,這樣的效果我是可以接受
 

做個結論,比我預期的好很多,代表就連這麼冷門的項目都有一定的判斷能力是我始料未及的,所以這已經可以當作某種極限測試

畢竟在現實商業應用上大概就是判斷商品圖等等,所以不錯,紀錄一下,有機會可以好好利用..

reference:

 

---

Before you read this, the article was in a superposition of human and AI. Upon observation, the wave function collapsed into a sleep-deprived developer.