Challenge
Level 7
This level is about image processing, inspired by pay-tv cracking. Code a program which is able to automatically unscramble the lines of a given image. Type in all characters from the image. Begin with the upper line, and add the lower line without a blank in between.
限时 180 秒:GET /missions/prog/7/ 生成随机实例(图在 /missions/prog/7/BMP),
POST /missions/prog/7/index.php 字段 solution。
Solution
Step 1: 图像结构
实例图是 200×100 的 PNG。
对像素做颜色统计:
shape (100, 200, 3)(245, 184, 143) px 1031 rows 28 span (3, 98) maxrow 95(129, 127, 77) px 871 rows 27 span (7, 91) maxrow 65(113, 82, 86) px 6 rows 6 span (18, 81) maxrow 1(113, 66, 99) px 5 rows 5 span (20, 91) maxrow 1- 只有两个颜色的像素数在 800 以上(1031 / 871),它们就是两行文字的字形颜色;
- 其余颜色每个只有 4–6 个像素,是逐像素噪声(不是行级噪声);
- 两种字形色各自的 27–28 行散布在整幅图的高度上(span 3–98 / 7–91),说明加扰是行置换: 原始图的两行文字,它们的像素行被打乱后均匀撒在整张图里。
这就给出还原思路:先判断每一行原本在图像里的纵坐标,再把每个颜色的行按这个坐标排序,就能把两行文字拼回来。
Step 2: 行序判定
背景是一条垂直渐变,B 通道随行号单调变化;而行置换并不改变每一行的像素内容, 于是每一行自身的 B 通道统计量就保留了它原来的纵坐标信息。实测 (midrange)足够稳定:
import numpy as npfrom PIL import Image
a = np.array(Image.open("chal_live.bmp").convert("RGB")).astype(int)H, W, _ = a.shapebm = np.array([(a[y, :, 2].min() + a[y, :, 2].max()) / 2.0 for y in range(H)])order = sorted(range(H), key=lambda y: bm[y]) # 按估计出的原始位置排序rank = {y: k for k, y in enumerate(order)} # rank 0 == 原图最下面一行对每个字形色取出它的行、按 rank 排序后渲染(白底黑字、5 倍放大),就得到两段干净的字形条带:
colour A (129, 127, 77): 27 rows -> ranks [8, 10, 12, ..., 46, 48, 49]colour B (212, 179, 160): 27 rows -> ranks [50, 51, 52, ..., 75, 76]mean ranks {'A': 29.5, 'B': 63.0} -> submit order: strip B first, then A注意这里的 rank 是从下往上计数的(rank 0 = 底部):色带 B 的 rank 均值更大,
说明它原本位于上面那一行;所以拼接顺序必须是 B 的文字 + A 的文字。
Step 3: 读出字符
条带放大 5 倍后字形是纯黑白的点阵,逐像素打成 ASCII
line 1 (strip B): J 6 7 L 3 6line 2 (strip A): M E 5 F E YVerify
同一个实例的提交实测(每行是 solution=<串> 的返回判定):
submit 'ME5FEYJ67L36' -> ok=False bad=True # 先上后下、但左右顺序错submit 'J67L36ME5FEY' -> ok=True bad=False # 接受submit 'ME5FEY' -> ok=False bad=True # 只交一行不行submit 'J67L36' -> ok=False bad=True