HackThisSite - Programming Mission 6
Challenge
Bypass the image captcha
This level is about OCR. Write a program which is able to read all the characters in the given image, and let it beat this image captcha automatically. Type in all characters from the image which you can find HERE. Begin from the innermost character and continue clockwise. You have 30 seconds time to send the solution.
Solution
Step 1: 端点数据
实例页 https://www.hackthissite.org/missions/prog/6/
里的链接指向 /missions/prog/6/image。直接 GET 它拿到的是
HTML(Content-Type: text/html),
内容是真正的 drawIt()(完整数组约 3350
个整数,下面只列开头)。这意味着题目虽然把它描述成 OCR,
但客户端拿到的并不是一张
PNG,而是可以直接重建的绘图指令;“随螺旋倒置”的字符只是整体旋转,
不需要把倒置字符单独当作另一套字形:先按极角把每个字符旋回统一方向,再识别即可。
常规 OCR 在本题的螺旋排列上不稳定。下面按连通域切分、按极角旋转归一化,再对矢量字形做模板匹配。
1 | $ curl -sL -b '<mission-cookie>' 'https://www.hackthissite.org/missions/prog/6/image/' |
Step 2: 图元与字形
把图元按原样画进一张 idmap[x][y] = 第几个图元
的栅格图,然后对栅格做 8 邻域洪泛:
连成一片的像素就是一个字形,顺便把该字形用到的图元 id
全部收集起来。
这样字形切分完全不依赖列投影之类的排版假设,对螺旋上各种朝向的字形都成立。
1 | import math |
Step 3: 螺旋排序
题面已经给了读序:从最内层开始,顺时针。字形落在一条对数螺旋上:每 10 度放一个字形, 半径每步乘 1.005。于是读序 = 第几圈(半径)主序 + 圈内第几个(角度)次序。
先网格搜索螺旋中心:让所有字形的极角落进 10 度格子(最内圈的几个字形半径太小、 角度噪声大,直接跳过不参与打分)。再把序号 \(k = j + 36m\)(\(j\) 为圈内角序,\(m\) 为圈号)迭代拟合 \(\ln r = A + B \cdot k\)(\(B = \ln 1.005\)),直到分配稳定:
1 | from collections import Counter |
Step 4: 原型提取
字符集是大写十六进制(0123456789ABCDEF),而且字形是矢量定义的:同一个字符的路径
形状每次完全一样,只是被整体旋转过(旋转角等于它所在位置的极角 + 90
度,让字形的上
朝外)。所以只要把每个字形转回标准朝向,同一字符的点云就能完全重合。
做法:把每个字形的图元绕自身中心反向旋转 -(ang + 90)
度,再把图元采成点云、
减去自身均值(消掉平移),就得到朝向归一化的点云。
1 | def rotate_prim(p, cx, cy, deg): |
离线阶段把 253 个点云两两算 Chamfer
距离、做完全连接层次聚类,t = 0.80 正好切出 16
类,和字符集大小一致(这一步本身就是 16
类假设的验证:多一类少一类都说明阈值错了)。 每类取
medoid(到同类其他成员平均距离最小的那个字形)作为原型,把 16 个原型打成
ASCII 点阵人眼看一遍,就得到标签表。下面是复核过的其中两个原型:
1 | label=8 medoid=k1 label=4 medoid=k38 |
16 个原型全部人工确认过一次。原型存成
templates_gen.py,
Step 5: 模板分类
点云之间用对称 Chamfer 距离比形状:对每个点找另一方最近点的距离,两个方向取平均。 比位图 IoU 更耐受采样密度差异,也不需要对齐网格。
完整的 templates_gen.py 内容如下。它是被
solve.py 导入的纯数据模块。
1 | # auto-generated canonical glyph templates (relative to cloud mean) |
分类函数在 solver 中使用这些模板:
1 | def classify(order, templates): |
Step 6: 在线 Solver
30 秒限时从实例页生成那一刻开始算,所以不能拆成几步手工执行,必须一个进程内完成, 而且 POST 前要重新算一次时间预算。给足余量后:
1 | def solve_once(cookie, dry=False): |
上面的 Python 块按顺序拼入同一个
solve6.py,TEMPLATES 也在该文件内;依赖为
requests、numpy 和
scipy。补上运行入口:
1 | if __name__ == "__main__": |
圆模板 O 是完整的 360 度圆,不带弧起始角;采样器必须与
A 弧分开处理。