
两个魔方
家里买了两个三阶魔方。
- 正版魔方:每格都有黑色边框,颜色集中在格子中间。白色中心块上有商标标志,而且表面有光泽,会反光。
- 韩国发售版:轻巧,转动顺滑。不过它是没有贴纸的荧光塑料,颜色很浓,而且很薄,背面的颜色会透出来。
先试 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
还没有评论。阅读时有疑问或想补充的内容,欢迎留言。
发表评论前,请用 Google 开始并登记昵称和邮箱。
用 Google 开始