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
也能验证这个判断,但它只能作为辅助而不是最终解法。把矢量指令渲染成
bitmap 后, 对整张螺旋图运行
Tesseract(--psm 3)在实测实例上直接得到
Empty page;即使改用 --psm 6/11/12,
输出也是断裂、乱序的片段。把字符按连通域切开、按所在极角旋转后逐字
OCR,实测一个 36 字符圈只能读出 约 34 个,且不同
psm、缩放和阈值会产生不同误识别。因此,常规 OCR
的正确用法是“渲染 + 分割 + 旋转 归一化 + 单字识别”,但要稳定读完 253
字符,仍需要对这些矢量字形做模板匹配;下面采用后者。
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 + 36 * 圈号 m 迭代拟合
ln r = A + B·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): |