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.
状态:verified(服务端返回
Good Job, ***, You have successfully completed this mission)。
$ curl -sL -b '<mission-cookie>' 'https://www.hackthissite.org/missions/prog/6/image/' \r\n<html>\r\n<head>\r\n\r\n<script type="text/javascript">\r\n<!--\r\n var strHTML = "";\t\r\n function drawIt()\r\n{\r\n var drawData = new Array(565,623,565,618,768,354,757,364,520,643,528,645, 794,563,782,556,658,452,672,448,700,663,697,656,490,519,486,520,363,464,
import numpy as np import requests from scipy.spatial import cKDTree
BASE = "https://www.hackthissite.org" N_CHARS = 253 STEP = 0.35# point-cloud sampling spacing, in device px
defsession(cookie): s = requests.Session() s.headers.update({ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", "Cookie": cookie, "Referer": BASE + "/missions/programming/", }) return s
deffetch_drawdata(s): """Instance page -> captcha URL -> the flat drawData array. Timer starts here.""" page = s.get(BASE + "/missions/prog/6/", timeout=20).text hit = re.search(r'(?:href|src)="([^"]*prog/6/image[^"]*)"', page) url = hit.group(1) if hit else"/missions/prog/6/image" ifnot url.endswith("/"): url += "/"# bare path 301s but the redirect body is empty if url.startswith("/"): url = BASE + url html = s.get(url, timeout=20, headers={"Referer": BASE + "/missions/prog/6/"}).text m = re.search(r"new Array\(([^)]*)\)", html, re.S) return [int(x) for x in re.findall(r"-?\d+", m.group(1))]
defprimitives(data): """Split drawData exactly the way drawIt() does: 4 ints = line, 5 ints = arc.""" i, out = 0, [] while i + 2 < len(data): if data[i + 2] >= 10: out.append(("L", *[float(v) for v in data[i:i + 4]])) i += 4 else: out.append(("A", *[float(v) for v in data[i:i + 5]])) i += 5 return out
defrasterize(prims, w, h): """idmap[x][y] = index of the primitive that painted that pixel.""" idmap = [[-1] * h for _ inrange(w)] for idx, p inenumerate(prims): if p[0] == "L": x1, y1, x2, y2 = p[1:5] n = max(2, int(math.hypot(x2 - x1, y2 - y1) * 2)) pts = [(x1 + (x2 - x1) * t / n, y1 + (y2 - y1) * t / n) for t inrange(n + 1)] else: x0, y0, r, s, e = p[1:6] n = max(2, int(abs(e) / 4.0)) pts = [(x0 + r * math.cos(math.radians(s + e * t / n)), y0 - r * math.sin(math.radians(s + e * t / n))) for t inrange(n + 1)] for fx, fy in pts: xx, yy = int(round(fx)), int(round(fy)) if0 <= xx < w and0 <= yy < h: idmap[xx][yy] = idx return idmap
defglyphs(idmap): """8-neighbour flood fill: one connected blob = one character.""" w, h = len(idmap), len(idmap[0]) seen = [[False] * h for _ inrange(w)] out = [] for sx inrange(w): for sy inrange(h): if idmap[sx][sy] < 0or seen[sx][sy]: continue stack, pts, ids = [(sx, sy)], [], set() seen[sx][sy] = True while stack: x, y = stack.pop() pts.append((x, y)) ids.add(idmap[x][y]) for dx in (-1, 0, 1): for dy in (-1, 0, 1): nx, ny = x + dx, y + dy if (0 <= nx < w and0 <= ny < h andnot seen[nx][ny] and idmap[nx][ny] >= 0): seen[nx][ny] = True stack.append((nx, ny)) iflen(pts) >= 4: # drop 1-3 px specks xs = [q[0] for q in pts] ys = [q[1] for q in pts] out.append({"ids": sorted(ids), "cx": sum(xs) / len(xs), "cy": sum(ys) / len(ys), "n": len(pts)}) return out
deffit_center(gs, init): """Grid-search the spiral centre so every glyph angle lands on the 10-deg lattice.""" defscore(cx, cy): tot, cnt = 0.0, 0 for g in gs: if math.hypot(g["cx"] - cx, g["cy"] - cy) < 150: continue ang = math.degrees(math.atan2(g["cy"] - cy, g["cx"] - cx)) tot += abs((ang + 5.0) % 10.0 - 5.0) ** 2 cnt += 1 return tot / max(1, cnt)
best = (score(*init), init[0], init[1]) for span, step in ((80.0, 1.0), (4.0, 0.2), (0.6, 0.05)): x0, y0 = best[1], best[2] x = x0 - span while x <= x0 + span: y = y0 - span while y <= y0 + span: sc = score(x, y) if sc < best[0]: best = (sc, x, y) y += step x += step return best[1], best[2]
deforder_glyphs(gs, cx, cy): """k = angle index + 36 * turn; k = 0 is the innermost glyph, k grows clockwise.""" for g in gs: g["r"] = math.hypot(g["cx"] - cx, g["cy"] - cy) g["ang"] = math.degrees(math.atan2(g["cy"] - cy, g["cx"] - cx)) % 360.0 g["slot"] = int(round(g["ang"] / 10.0)) % 36 cnt = Counter(g["slot"] for g in gs) j0 = max(cnt, key=lambda s: cnt[s]) # innermost slot holds the extra glyph for g in gs: g["j"] = (g["slot"] - j0) % 36 A = math.log(min(g["r"] for g in gs)) for _ inrange(8): # iterate ln r = A + B*k for g in gs: m = round((math.log(g["r"]) - A - B_LOG * g["j"]) / (36.0 * B_LOG)) m = min(max(m, 0), 7) k = g["j"] + 36 * m g["k"] = k if0 <= k <= N_CHARS - 1elseNone pts = [(g["k"], math.log(g["r"])) for g in gs if g["k"] isnotNone] n = len(pts) mk = sum(a for a, b in pts) / n mr = sum(b for a, b in pts) / n slope = (sum((a - mk) * (b - mr) for a, b in pts) / sum((a - mk) ** 2for a, b in pts)) A = mr - slope * mk returnsorted([g for g in gs if g["k"] isnotNone], key=lambda g: g["k"])
defrotate_prim(p, cx, cy, deg): """Rotate a primitive about (cx, cy) by deg degrees in screen coords.""" th = math.radians(deg) c, s = math.cos(th), math.sin(th)
defR(x, y): dx, dy = x - cx, y - cy return (cx + dx * c - dy * s, cy + dx * s + dy * c)
if p[0] == "L": x1, y1 = R(p[1], p[2]) x2, y2 = R(p[3], p[4]) return ("L", x1, y1, x2, y2) x, y = R(p[1], p[2]) return ("A", x, y, p[3], p[4] - deg, p[5]) # the start angle shifts too
defsample(prims): """Sample canonical primitives to a point cloud, centred on its own mean.""" pts = [] for p in prims: if p[0] == "L": x1, y1, x2, y2 = p[1:5] n = max(2, int(math.hypot(x2 - x1, y2 - y1) / STEP)) pts += [(x1 + (x2 - x1) * t / n, y1 + (y2 - y1) * t / n) for t inrange(n + 1)] else: x0, y0, r, s, e = p[1:6] n = max(2, int(abs(e) / 4.0)) pts += [(x0 + r * math.cos(math.radians(s + e * t / n)), y0 - r * math.sin(math.radians(s + e * t / n))) for t inrange(n + 1)] a = np.array(pts, dtype=np.float64) a -= a.mean(0) return a
defcloud(g): """Orientation-normalised point cloud of one glyph.""" ang = round(g["ang"] / 10.0) * 10.0 return sample([rotate_prim(p, g["cx"], g["cy"], -(ang + 90.0)) for p in g["prims"]])
TEMPLATES = { # char -> list of canonical primitives, auto-generated offline }
defclassify(order, templates): tchars = sorted(templates) tclouds = {c: sample(templates[c]) for c in tchars} trees = {c: cKDTree(tclouds[c]) for c in tchars} chars, margins = [], [] for g in order: c = cloud(g) ct = cKDTree(c) ranked = sorted((0.5 * (trees[t].query(c)[0].mean() + ct.query(tclouds[t])[0].mean()), t) for t in tchars) chars.append(ranked[0][1]) margins.append(ranked[1][0] - ranked[0][0]) return"".join(chars), min(margins)
自测里这个判据非常干净:253 个字形零误分类,且最近
vs 次近的最小间隔是 0.26 px (中位数 0.72
px)。也就是说没有任何一个字形的两候选挨得很近,分类结果没有模糊地带。
6. 在线 solver
30
秒限时从实例页生成那一刻开始算,所以不能拆成几步手工执行,必须一个进程内完成,
而且 POST 前要重新算一次时间预算。给足余量后:
defsolve_once(cookie, dry=False): s = session(cookie) t0 = time.time()
data = fetch_drawdata(s) # timer starts here prims = primitives(data) w = int(max(p[1] for p in prims)) + 30 h = int(max(p[2] for p in prims)) + 30 gs = glyphs(rasterize(prims, w, h)) init = (sum(g["cx"] for g in gs) / len(gs), sum(g["cy"] for g in gs) / len(gs)) cx, cy = fit_center(gs, init) order = order_glyphs(gs, cx, cy) for g in order: ids = set(g["ids"]) g["prims"] = [p for i, p inenumerate(prims) if i in ids] answer, margin = classify(order, TEMPLATES) t_solve = time.time()
if dry: return answer, t_solve - t0 resp = s.post(BASE + "/missions/prog/6/index.php", data={"solution": answer, "submitbutton": "submit"}, timeout=30, headers={"Referer": BASE + "/missions/prog/6/"}).text ok = "successfully completed"in resp or"Congrats"in resp return answer, time.time() - t0, ok