큐브 두 개

집에 3×3을 두 개 샀습니다.

  • 정발품: 칸마다 검은 테두리가 있고 색이 칸 가운데에 몰려 있습니다. 흰 중앙에는 상표 로고가 있고, 광택 때문에 빛이 반사됩니다.
  • 한국발매품: 가볍고 부드럽게 돕니다. 대신 스티커 없는 형광 플라스틱이라 색이 진하고, 얇아서 뒤쪽 색이 비쳐 보입니다.

VLM으로 먼저

바둑에 쓰던 Qwen3-VL-8B를 붙여 봤습니다. 사진 두 장을 한 번에 보여 주고 면마다 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의 정체와 실제 인식률, 그리고 배포’입니다.