
큐브 두 개
집에 3×3을 두 개 샀습니다.
- 정발품: 칸마다 검은 테두리가 있고 색이 칸 가운데에 몰려 있습니다. 흰 중앙에는 상표 로고가 있고, 광택 때문에 빛이 반사됩니다.
- 한국발매품: 가볍고 부드럽게 돕니다. 대신 스티커 없는 형광 플라스틱이라 색이 진하고, 얇아서 뒤쪽 색이 비쳐 보입니다.
VLM으로 먼저
바둑에 쓰던 Qwen3-VL-8B를 붙여 봤습니다. 사진 두 장을 한 번에 보여 주고 면마다 9칸 색을 글자로 답하게 했습니다.
결과는 일반 스캔보다 나빴습니다. 색을 거의 구분하지 못했고, 한 면을 RRRRRRRRR처럼 한 가지 색으로 채우는 답이 자주 나왔습니다. 9칸 위치를 정확히 짚어야 하는 일에는 맞지 않았습니다. 지금 VLM 스캔은 빈칸이나 한 색으로 채운 면을 버리고 일반 스캔 결과로 바꾸는 보조 버튼으로만 남아 있습니다.
C++ 색 분해
그래서 C++로 직접 읽었습니다.
- 사진에서 큐브 영역을 찾고, 큰 사진은 1280px로 줄입니다.
- 흰 균형을 맞추고, 면 세 개의 격자 후보를 여러 개 만들어 칸 안쪽 색의 평균을 냅니다. 테두리와 반사를 피하려고 가장자리는 뺍니다.
- HSV와 RGB 조건으로 여섯 색 중 하나로 나눕니다.
- 중앙 색으로 면을 정하고 노랑 → 빨강 → 초록 → 주황 → 파랑 → 흰색 순으로 한 면씩 확정합니다.
- 어느 사진이 흰색 위인지는 상표 로고와 노랑·흰색 비율로 알아냅니다. 사진 순서를 바꿔 넣어도 결과가 같았습니다.
하나를 맞추면 하나가 깨짐
한국발매품을 먼저 맞췄습니다. 형광 초록이 노랑으로, 비친 색이 다른 면 색으로 읽히는 문제를 하나씩 잡아 처음 사진은 다 맞게 했습니다.
그다음 더 쉬워 보이던 정발품을 넣었는데, 처음에는 여섯 면 중 세 면만 찾았습니다.

정발품을 처음 읽은 화면입니다. 위·왼·오른 면을 찾지 못했습니다.
검은 테두리는 칸을 나누는 데는 도움이 되지만, 칸 안쪽을 조금만 벗어나도 검정과 책상 색이 섞여 흰색과 초록을 헷갈렸습니다. 정발품은 주황과 빨강도 가깝습니다. 정발품용 조건을 넣으면 한국발매 사진이 다시 깨졌고, 두 큐브를 번갈아 맞추는 일이 반복됐습니다.
색 분류가 들어 있는 CubeModel.cpp에는 소수 기준값이 230개, 사진 인식 파일인 CubePhotoReconstructor.cpp에는 379개 있습니다. 정답을 아는 사진 몇 장에 맞춰 손으로 고른 값들입니다.
이 단계의 실제 코드
노랑과 흰색을 가르는 조건입니다. 노랑은 초록 성분이 빨강과 비슷해야 하고, 그래야 주황과 갈립니다.
src/CubeModel.cpp
// --- 강한/옅은 특징 ---
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.cpp
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의 정체와 실제 인식률, 그리고 배포’입니다.
COMMENTS · CHAPTER 02
VLM 대신 색 분해로, 성격이 다른 두 큐브 댓글 0
아직 댓글이 없습니다. 읽다가 궁금한 점이나 보탤 내용을 남겨 주세요.
댓글을 등록하려면 구글로 시작하기를 눌러 닉네임과 메일 주소를 등록해 주세요.
구글로 시작하기