"""第136話 1コマ目の床の線を、絵の上に正確に描く(2026-11-05) エイイチ「左の線が1本ない・大城さんもレーンの真ん中を通る」→ 線は絵のモデルに任せず、遠近の計算で4本引く。 大城さんの絵(床に何もない版)を切り抜いて、歩く人のレーンの真ん中に立たせる。マークは別の絵(071045)から切り出した黒い形。""" import sys, numpy as np from PIL import Image, ImageDraw from scipy import ndimage src, picsrc, out = sys.argv[1:4] im = Image.open(src).convert("RGB"); W, H = im.size a = np.asarray(im).astype(int) green = (a[..., 1] - (a[..., 0] + a[..., 2]) / 2) > 40 fig = ndimage.binary_fill_holes(ndimage.binary_opening(~green, iterations=2)) ys, xs = np.nonzero(fig); t0, b0, l0, r0 = ys.min(), ys.max(), xs.min(), xs.max() fx0 = xs[ys > b0 - 0.04 * H].mean() vx, vy = 0.50 * W, -0.35 * H # 消える点を画面の上の外に置き、線が集まりすぎないようにする XB = [0.02 * W, 0.42 * W, 0.58 * W, 0.98 * W] # 下の端での4本の線 def X(x, y): return vx + (x - vx) * (y - vy) / (H - vy) FEET, HGT = 0.80 * H, 0.60 * H # 大城さんの足もとと背の高さ cx = X((XB[0] + XB[1]) / 2, FEET) # 歩く人のレーンの真ん中 s = HGT / (b0 - t0) lay = Image.new("L", (W, H), 0); d = ImageDraw.Draw(lay) for x in XB: for k in range(80): y0 = 0.10 * H + 0.90 * H * k / 80; y1 = 0.10 * H + 0.90 * H * (k + 1) / 80 + 1 w0 = w1 = 26 # 4本とも同じ太さ d.polygon([(X(x, y0) - w0 / 2, y0), (X(x, y0) + w0 / 2, y0), (X(x, y1) + w1 / 2, y1), (X(x, y1) - w1 / 2, y1)], fill=255) def icon(name, S=400): """床のマーク(黒一色のピクトグラム)を描く""" m = Image.new("L", (S, S), 0); g = ImageDraw.Draw(m); L = lambda pts, w: g.line(pts, fill=255, width=w, joint="curve") if name == "walk": g.ellipse((175, 10, 245, 80), fill=255) # 頭 L([(200, 100), (175, 230)], 52) # 胴 L([(195, 120), (120, 185), (80, 160)], 34); L([(195, 120), (260, 175), (300, 210)], 34) # 腕 L([(175, 225), (120, 300), (60, 380)], 40); L([(175, 225), (240, 300), (270, 385)], 40) # 脚 else: L([(20, 40), (80, 40), (130, 280), (360, 280)], 26) # 取っ手と台 g.polygon([(110, 110), (380, 110), (350, 250), (135, 250)], fill=255) # かご g.ellipse((120, 300, 190, 370), fill=255); g.ellipse((300, 300, 370, 370), fill=255) # 車輪 return m.crop(m.getbbox()) def put(m, lane, y, frac): lw = (XB[lane + 1] - XB[lane]) * (y - vy) / (H - vy); r = lw * frac / m.width m = m.resize((max(1, int(m.width * r)), max(1, int(m.height * r * 0.7)))) c = X((XB[lane] + XB[lane + 1]) / 2, y) lay.paste(255, (int(c - m.width / 2), int(y - m.height / 2)), m) put(icon("walk"), 0, 0.895 * H, 0.38) # 歩く人:大城さんの手前 put(icon("cart"), 2, 0.82 * H, 0.50) # 台車:となりのレーン base = np.zeros((H, W, 3), np.uint8); base[:] = (0, 177, 64) base[np.asarray(lay) > 128] = (0, 0, 0) B = Image.fromarray(base) f = im.crop((l0, t0, r0 + 1, b0 + 1)); mk = Image.fromarray((fig[t0:b0 + 1, l0:r0 + 1] * 255).astype(np.uint8)) f = f.resize((int(f.width * s), int(f.height * s)), Image.LANCZOS); mk = mk.resize(f.size, Image.LANCZOS) B.paste(f, (int(cx - (fx0 - l0) * s), int(FEET - f.height)), mk) B.save(out, quality=95) print("レーンの真ん中", int(cx), "足もと", int(FEET), "縮め", round(s, 2))