二つのキューブ

家用に 3×3 を二つ買いました。

  • 正規品: マスごとに黒い枠があり、色がマスの中央に寄っています。白の中央にはブランドのロゴがあり、光沢のせいで光が反射します。
  • 韓国発売品: 軽くて滑らかに回ります。その代わりステッカーのない蛍光プラスチックなので色が濃く、薄いため裏側の色が透けて見えます。

まず VLM で

囲碁に使っていた Qwen3-VL-8B をつないでみました。写真2枚を一度に見せ、面ごとに9マスの色を文字で答えさせました。

結果は通常のスキャンより悪いものでした。色をほとんど区別できず、一つの面を RRRRRRRRR のように一色で埋める答えがよく出ました。9マスの位置を正確に押さえる必要がある仕事には向いていませんでした。今の VLM スキャンは、空欄や一色で埋めた面を捨てて通常のスキャンの結果に置き換える、補助のボタンとしてだけ残っています。

C++ の色の分解

そこで C++ で直接読み取りました。

  1. 写真からキューブの領域を探し、大きな写真は1280pxに縮めます。
  2. ホワイトバランスを整え、三つの面の格子の候補をいくつも作って、マスの内側の色の平均を取ります。枠と反射を避けるために端は除きます。
  3. HSV と RGB の条件で六色のどれかに分けます。
  4. 中央の色で面を決め、黄 → 赤 → 緑 → オレンジ → 青 → 白の順に一面ずつ確定します。
  5. どちらの写真が白が上なのかは、ブランドのロゴと黄・白の割合で割り出します。写真の順番を入れ替えて入れても結果は同じでした。

一つを合わせると一つが崩れる

まず韓国発売品に合わせました。蛍光の緑が黄に、透けた色が別の面の色に読まれる問題を一つずつ潰し、最初の写真はすべて正しく読めるようにしました。

次に、もっと簡単そうに見えた正規品を入れましたが、最初は六つの面のうち三つしか見つけられませんでした。

正規品を最初に読み取った画面です。上・左・右の面を見つけられませんでした。

黒い枠はマスを分けるのには役立ちますが、マスの内側から少しでもはみ出すと黒や机の色が混ざり、白と緑を取り違えました。正規品はオレンジと赤も近い色です。正規品用の条件を入れると韓国発売品の写真がまた崩れ、二つのキューブを交互に合わせることが繰り返されました。

色の分類が入っている CubeModel.cpp には小数の基準値が230個、写真認識のファイルである CubePhotoReconstructor.cpp には379個あります。正解が分かっている数枚の写真に合わせて、手で選んだ値です。

この段階の実際のコード

黄と白を分ける条件です。黄は緑の成分が赤と近い必要があり、それによってオレンジとも分かれます。

src/CubeModel.cppcpp · 12行
// --- 강한/옅은 특징 ---
const bool strongWhite = (v > 0.62 && s < 0.22 && spread < 0.20)
    || (v > 0.72 && s < 0.28 && spread < 0.25);
const bool paleWhite = (v > 0.55 && s < 0.32 && spread < 0.28)
    || (v > 0.60 && s < 0.38 && deg >= 170.0 && deg < 255.0 && !blueChrome && spread < 0.32);
const bool strongYellow = (v > 0.40 && r > 0.42 && g > 0.38 && b < 0.50
                           && (r + g) > b * 2.4 && deg >= 40.0 && deg < 95.0
                           && g >= r * 0.72 // 노랑: G≈R, 주황(G≪R) 제외
                           && !(g > r + 0.14 && s > 0.45));
const bool paleYellow = (v > 0.38 && r > 0.35 && g > 0.32 && b < 0.58
                         && (r + g) > b * 1.9 && deg >= 38.0 && deg < 100.0
                         && g >= r * 0.70 && !greenChrome);

正規品の白い中央のロゴを探す関数です。明るい点と暗い点の割合で見ます。

src/CubePhotoReconstructor.cppcpp · 26行
bool hasWhiteCenterLogo(const QImage &img, int cx, int cy, int radius)
{
    if (img.isNull() || radius < 2)
        return false;
    int bright = 0, dark = 0, n = 0;
    const int r = std::max(4, radius);
    for (int dy = -r; dy <= r; ++dy) {
        for (int dx = -r; dx <= r; ++dx) {
            if (dx * dx + dy * dy > r * r)
                continue;
            const int x = std::clamp(cx + dx, 0, img.width() - 1);
            const int y = std::clamp(cy + dy, 0, img.height() - 1);
            const QColor c = img.pixelColor(x, y);
            const int sum = c.red() + c.green() + c.blue();
            const int spr = std::max({c.red(), c.green(), c.blue()}) - std::min({c.red(), c.green(), c.blue()});
            ++n;
            if (sum > 400 && spr < 55)
                ++bright;
            else if (sum < 160)
                ++dark;
        }
    }
    if (n < 20)
        return false;
    return bright * 5 >= n * 2 && dark * 20 >= n && dark * 4 <= n;
}

次の記事は「3. 54/54 の正体と実際の認識率、そして配布」です。