|
| 1 | +# 马赛克 |
| 2 | + |
| 3 | +这道题的简单题意就是,给你一个打了码的二维码,并且给出打码的程序(算法是模糊部分的一个大的 block 的颜色,是这个区域内颜色的平均值)。<del>简单算一下信息量,感觉还是能复原的。</del> |
| 4 | + |
| 5 | +一些用词(为了避免混乱所以先说明):二维码的块:指模糊前的二维码的一个黑白块;码的块:指打码部分的一个色块(应该没毛病吧)。 |
| 6 | + |
| 7 | +于是乎,最朴素的方法就是……枚举原二维码每个块的黑白,然后看看跟模糊后的是否一致,但是这样做时间成本太大了。 |
| 8 | + |
| 9 | +然后可以发现,这个码的一个块的影响范围是有限的,大概就 10 个左右的二维码块。于是我们就可以针对每个码的块,枚举其下的二维码的黑白。再把每一个块的有效方案拼起来即可。 |
| 10 | + |
| 11 | +<del>原理听懂了吗?原理大概就这个样子。但是……你发现这个确实并不好实现(大概?)</del> |
| 12 | + |
| 13 | +具体实现的话,假设前面处理了若干个块,这些块拼在一起的有效方案(可能有多个)放在一个数组 `p` 里。然后处理一个新的块的时候,就把当前区域的方案,与 `p` 中的匹配(能相容的就结合),把新的方案放到一个新的数组 `q` 供下一轮使用。 |
| 14 | + |
| 15 | +具体代码(调试输出十分炫酷,建议运行): |
| 16 | + |
| 17 | +```python |
| 18 | +import random |
| 19 | +import math |
| 20 | +import numpy as np |
| 21 | +from PIL import Image |
| 22 | + |
| 23 | +X, Y = 103, 137 # 马赛克左上角位置(单位为像素) |
| 24 | +N = 20 # 马赛克块的数量(共N*N块) |
| 25 | +BOX_SIZE = 23 # 每个马赛克块的大小(边长,单位为像素) |
| 26 | +PIXEL_SIZE = 11 # 二维码每个块的大小(边长,单位为像素) |
| 27 | + |
| 28 | + |
| 29 | +def calc_block_color(img, x, y): |
| 30 | + x1 = X + x * BOX_SIZE |
| 31 | + x2 = X + (x + 1) * BOX_SIZE |
| 32 | + y1 = Y + y * BOX_SIZE |
| 33 | + y2 = Y + (y + 1) * BOX_SIZE |
| 34 | + return math.floor(img[x1:x2, y1:y2].mean()) |
| 35 | + |
| 36 | + |
| 37 | +def check_qr_block_in(mx, my, qx, qy): |
| 38 | + # 检查 (qx, qy) 这个二维码块 是否影响 (mx, my) 这个打码块 |
| 39 | + X1 = X + mx * BOX_SIZE |
| 40 | + X2 = X + (mx + 1) * BOX_SIZE - 1 |
| 41 | + Y1 = Y + my * BOX_SIZE |
| 42 | + Y2 = Y + (my + 1) * BOX_SIZE - 1 |
| 43 | + |
| 44 | + x1 = qx * PIXEL_SIZE |
| 45 | + x2 = (qx + 1) * PIXEL_SIZE - 1 |
| 46 | + y1 = qy * PIXEL_SIZE |
| 47 | + y2 = (qy + 1) * PIXEL_SIZE - 1 |
| 48 | + |
| 49 | + if (X1 <= x1 <= X2 and Y1 <= y1 <= Y2) or (X1 <= x1 <= X2 and Y1 <= y2 <= Y2) \ |
| 50 | + or (X1 <= x2 <= X2 and Y1 <= y1 <= Y2) or (X1 <= x2 <= X2 and Y1 <= y2 <= Y2): |
| 51 | + return True |
| 52 | + return False |
| 53 | + |
| 54 | + |
| 55 | +def paint_with(img, x, y, color): |
| 56 | + # 将 (x, y) 二维码块涂成 color 色 |
| 57 | + x1 = x * PIXEL_SIZE |
| 58 | + x2 = (x + 1) * PIXEL_SIZE |
| 59 | + y1 = y * PIXEL_SIZE |
| 60 | + y2 = (y + 1) * PIXEL_SIZE |
| 61 | + img[x1:x2, y1:y2] = color |
| 62 | + |
| 63 | + |
| 64 | +class Plan: |
| 65 | + def __init__(self, plan = None): |
| 66 | + if plan is None: |
| 67 | + plan = {} |
| 68 | + self.plan = plan |
| 69 | + self.min_x = 10000 |
| 70 | + self.min_y = 10000 |
| 71 | + |
| 72 | + def compatible(self, other): |
| 73 | + # 检查此 plan 是否与 `other` 兼容 |
| 74 | + for (x, y), value in self.plan.items(): |
| 75 | + if (data := other.plan.get((x, y))) is not None and data != value: |
| 76 | + return False |
| 77 | + return True |
| 78 | + |
| 79 | + def push(self, x, y, data): |
| 80 | + self.min_x = min(self.min_x, x) |
| 81 | + self.min_y = min(self.min_y, y) |
| 82 | + self.plan[(x, y)] = data |
| 83 | + |
| 84 | + def combine(self, other): |
| 85 | + # 将此 plan 与 `other` 结合,返回新的 plan |
| 86 | + assert self.compatible(other) |
| 87 | + new = Plan(self.plan.copy()) |
| 88 | + for (x, y), value in other.plan.items(): |
| 89 | + new.push(x, y, value) |
| 90 | + return new |
| 91 | + |
| 92 | + |
| 93 | +def get_initial_plan(img): |
| 94 | + # 这是获取初始的 plan,主要包含码周边的一圈二维码块 |
| 95 | + |
| 96 | + X1 = X + 0 * BOX_SIZE |
| 97 | + X2 = X + N * BOX_SIZE |
| 98 | + Y1 = Y + 0 * BOX_SIZE |
| 99 | + Y2 = Y + N * BOX_SIZE |
| 100 | + |
| 101 | + def is_in(x, y): |
| 102 | + return X1 <= x <= X2 and Y1 <= y <= Y2 |
| 103 | + |
| 104 | + plan = Plan() |
| 105 | + |
| 106 | + for x, y in np.ndindex(57, 57): |
| 107 | + for i, j in np.ndindex(N, N): |
| 108 | + if check_qr_block_in(i, j, x, y): |
| 109 | + x1 = x * PIXEL_SIZE |
| 110 | + x2 = (x + 1) * PIXEL_SIZE - 1 |
| 111 | + y1 = y * PIXEL_SIZE |
| 112 | + y2 = (y + 1) * PIXEL_SIZE - 1 |
| 113 | + |
| 114 | + if not is_in(x1, y1): |
| 115 | + color = img.getpixel((y1, x1)) |
| 116 | + plan.push(x, y, color) |
| 117 | + break |
| 118 | + elif not is_in(x2, y2): |
| 119 | + color = img.getpixel((y2, x2)) |
| 120 | + plan.push(x, y, color) |
| 121 | + break |
| 122 | + |
| 123 | + POSITION_BLOCK = [(50, 50), (50, 28), (28, 50), (28, 28)] # 定位块 |
| 124 | + for block in POSITION_BLOCK: |
| 125 | + for x, y in np.ndindex(5, 5): |
| 126 | + d = max(abs(x - 2), abs(y - 2)) |
| 127 | + color = 0 if (d % 2 == 0) else 255 |
| 128 | + plan.push(block[0] + x - 2, block[1] + y - 2, color) |
| 129 | + |
| 130 | + return plan |
| 131 | + |
| 132 | + |
| 133 | +def iter_blocks(): |
| 134 | + # 枚举顺序从四周到中心,方便先利用已知数据 |
| 135 | + for d in range(N): |
| 136 | + for i, j in np.ndindex(N, N): |
| 137 | + dx = min(i, (N - 1) - i) |
| 138 | + dy = min(j, (N - 1) - j) |
| 139 | + if min(dx, dy) == d: |
| 140 | + yield (j, i) |
| 141 | + |
| 142 | + |
| 143 | +def main(): |
| 144 | + mosaic = Image.open('pixelated_qrcode.bmp') |
| 145 | + temp = np.asarray(mosaic.copy(), dtype='uint8') |
| 146 | + |
| 147 | + plans = [ get_initial_plan(mosaic) ] |
| 148 | + for i, j in iter_blocks(): |
| 149 | + new_plans = [] |
| 150 | + |
| 151 | + related_block = [] |
| 152 | + for x, y in np.ndindex(57, 57): |
| 153 | + if check_qr_block_in(i, j, x, y): |
| 154 | + related_block.append([x, y]) |
| 155 | + n = len(related_block) |
| 156 | + |
| 157 | + print(f'Solving ({i:2}, {j:2}), related blocks: {n}') |
| 158 | + |
| 159 | + for b in range(2 ** n): |
| 160 | + plan = Plan() |
| 161 | + for index, block in enumerate(related_block): |
| 162 | + if ((2 ** index) & b) > 0: |
| 163 | + paint_with(temp, *block, 0) |
| 164 | + plan.push(*block, 0) |
| 165 | + else: |
| 166 | + paint_with(temp, *block, 255) |
| 167 | + plan.push(*block, 255) |
| 168 | + |
| 169 | + color = calc_block_color(temp, i, j) |
| 170 | + correct_color = mosaic.getpixel((Y + j * BOX_SIZE, X + i * BOX_SIZE)) |
| 171 | + if color == correct_color: |
| 172 | + for old_plan in plans: |
| 173 | + if plan.compatible(old_plan): |
| 174 | + new_plans.append(old_plan.combine(plan)) |
| 175 | + |
| 176 | + plans = new_plans |
| 177 | + |
| 178 | + if len(plans) > 1000: |
| 179 | + ids = [ i for i in range(len(plans)) ] |
| 180 | + random.shuffle(ids) |
| 181 | + buckets = [] |
| 182 | + for i in ids[:1000]: |
| 183 | + buckets.append(plans[i]) |
| 184 | + plans = buckets |
| 185 | + |
| 186 | + if len(plans) == 0: # 如果 Failed 了,请洗把脸回来,再跑一遍 |
| 187 | + raise Exception('Failed') |
| 188 | + |
| 189 | + # 比较炫酷的调试输出 |
| 190 | + for x in range(9, 52): |
| 191 | + for y in range(12, 55): |
| 192 | + if (value := plans[0].plan.get((x, y))) is not None: |
| 193 | + c = '*' if value == 0 else '_' |
| 194 | + else: |
| 195 | + c = ' ' |
| 196 | + print(c, end=' ') |
| 197 | + print() |
| 198 | + print(f'Current valid plans: {len(plans)}') |
| 199 | + |
| 200 | + # 输出 10 张复原的二维码 |
| 201 | + for i, plan in enumerate(plans[:10]): |
| 202 | + for (x, y), value in plan.plan.items(): |
| 203 | + paint_with(temp, x, y, value) |
| 204 | + image = Image.fromarray(temp, mode='L') |
| 205 | + image.save(f'result-{i}.bmp') |
| 206 | + |
| 207 | + |
| 208 | +if __name__ == '__main__': |
| 209 | + main() |
| 210 | +``` |
0 commit comments