"""画像認識と物体検出(/series7)の記事サムネイル12本を作る(2026-11-08)。 トップのカードと同じく、実写の写真と、その回で計算した結果を組み合わせる。並べ方は回ごとに変え、差し色は1枚に1色だけ。 数字はすべて記事の実測値か、この中で写真から計算した値。 素材(KIOXIA の r2r2r-data と、記事の作業フォルダ)は Mac 連携で /mnt/user-data/uploads/ に写してから動かす。 python3 make.py # 12本を out/ に PNG で書き出す 出力は out/LOG{N}.png(1920×1080)。webp にして public/thumbs/img/ へ置く(publish.py)。 """ import os, json, cv2, numpy as np from PIL import Image, ImageOps, ImageDraw, ImageFont U = '/mnt/user-data/uploads/KIOXIA/r2r2r-data/' V = U + 'vision/captured/' SITE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) W, H = 1920, 1080 BK = (17, 17, 17); WH = (241, 240, 236); MID = (128, 128, 124); DK = (40, 40, 40) RED = (224, 38, 44); BLUE = (30, 120, 210); AMBER = (240, 168, 0); ORANGE = (240, 120, 30) GREEN = (60, 200, 120); CYAN = (0, 170, 200); TEAL = (0, 160, 150) MONO = '/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf' IB = '/usr/share/fonts/opentype/inter/Inter-ExtraBold.otf' os.makedirs('out', exist_ok=True) def F(p, s): return ImageFont.truetype(p, s) def load(p): return np.array(ImageOps.exif_transpose(Image.open(p)).convert('RGB')) def gray(a): g = cv2.cvtColor(a, cv2.COLOR_RGB2GRAY); return np.stack([g] * 3, -1) def fit(a, w, h, interp=None): if interp is None: interp = cv2.INTER_AREA if a.shape[1] > w else cv2.INTER_LANCZOS4 return cv2.resize(a, (w, h), interpolation=interp) def cropc(a, cx, cy, w, h): x0 = int(max(0, min(a.shape[1] - w, cx - w / 2))); y0 = int(max(0, min(a.shape[0] - h, cy - h / 2))) return a[y0:y0 + h, x0:x0 + w], (x0, y0) def redmask(img, smin=110, vmin=50): h = cv2.cvtColor(img, cv2.COLOR_RGB2HSV) return (((h[..., 0] < 8) | (h[..., 0] > 170)) & (h[..., 1] > smin) & (h[..., 2] > vmin)).astype(np.uint8) def biggest(m): n, l, s, _ = cv2.connectedComponentsWithStats(m.astype(np.uint8)); return (l == 1 + np.argmax(s[1:, 4])).astype(np.uint8) def label(d, x, y, num, name, col): d.text((x, y), num, font=F(MONO, 30), fill=col); d.text((x, y + 42), name, font=F(MONO, 38), fill=col) def dashed(d, box, col, width=3, dl=16): x0, y0, x1, y1 = box for a, b in [((x0, y0), (x1, y0)), ((x1, y0), (x1, y1)), ((x1, y1), (x0, y1)), ((x0, y1), (x0, y0))]: a = np.array(a, float); b = np.array(b, float); n = max(2, int(np.linalg.norm(b - a) // dl)) for j in range(0, n, 2): q0 = a + (b - a) * j / n; q1 = a + (b - a) * (j + 1) / n; d.line([tuple(q0), tuple(q1)], fill=col, width=width) def save(im, n): im.save(f'out/LOG{n}.png'); print('LOG', n) # ---------- Log#1 LOG298:9つの層(写真 + 層の積み上げ。評価指標の層だけ差し色) def log298(): src = load(V + 'Log05/mat_white_ref_20260729_211605_f0.png') ph, _ = cropc(src, 960, 470, 860, 840); ph = fit(gray(ph), 1000, 1080) # 写真の幅 1000 im = Image.new('RGB', (W, H), WH); im.paste(Image.fromarray(ph), (0, 0)); d = ImageDraw.Draw(im) names = ['PIXELS & COLOR', 'CLASSICAL CV', 'CNN', 'DETECTION', 'SEGMENTATION & POSE', 'METRICS', 'DATA & TRAINING', 'LANGUAGE', 'RUNTIME'] x0, x1, top, rh, gap = 1080, 1840, 90, 88, 8 for i, nm in enumerate(names): # 下が第1層 y = top + (8 - i) * (rh + gap) hi = (i == 5) d.rectangle([x0, y, x1, y + rh], fill=AMBER if hi else (DK if i < 2 else (210, 209, 204))) col = BK if hi or i >= 2 else WH d.text((x0 + 28, y + 24), f'{i + 1}', font=F(MONO, 36), fill=col) d.text((x0 + 100, y + 24), nm, font=F(MONO, 36), fill=col) d.text((x0, top + 9 * (rh + gap) + 20), 'NINE LAYERS · METRICS LEARNED 3RD', font=F(MONO, 28), fill=BK) label(d, 40, 940, '01', 'PHOTO', WH) save(im, 298) # ---------- Log#2 LOG299:写真の中の1画素を拡大していく(写真 + 拡大したマス) def log299(): src = load(V + 'Log02/IMG_7901.JPG'); m = biggest(redmask(src)); ys, xs = np.nonzero(m); cx, cy = int(xs.mean()), int(ys.mean()) s = 1080 / 1500; w0 = int(1000 / s) x0s, y0s = cx - w0 // 2 + 80, cy - 750 ph = gray(fit(src[y0s:y0s + 1500, x0s:x0s + w0], 1000, 1080)) im = Image.new('RGB', (W, H), WH); im.paste(Image.fromarray(ph), (0, 0)); d = ImageDraw.Draw(im) zx, zy, zs = 480, 520, 110; n = 6; X0, Y0, c = 1100, 90, 130 d.rectangle([zx, zy, zx + zs, zy + zs], outline=RED, width=5) d.line([zx + zs, zy, X0, Y0], fill=RED, width=2); d.line([zx + zs, zy + zs, X0, Y0 + n * c - 4], fill=RED, width=2) sx = int(x0s + zx / s); sy = int(y0s + zy / s); sz = int(zs / s) cells = fit(src[sy:sy + sz, sx:sx + sz], n, n, cv2.INTER_AREA) for r in range(n): for k in range(n): R, G, B = [int(v) for v in cells[r, k]]; g = int(0.299 * R + 0.587 * G + 0.114 * B) fill = (R, G, B) if (r, k) == (2, 2) else (g, g, g) d.rectangle([X0 + k * c, Y0 + r * c, X0 + k * c + c - 4, Y0 + r * c + c - 4], fill=fill) d.text((X0 + k * c + 18, Y0 + r * c + 44), f'{R:3d}', font=F(MONO, 36), fill=WH if g < 130 or (r, k) == (2, 2) else BK) R, G, B = [int(v) for v in cells[2, 2]] d.text((X0, 930), f'R {R} G {G} B {B}', font=F(MONO, 38), fill=BK) d.text((X0, 985), 'ONE PIXEL = THREE NUMBERS', font=F(MONO, 28), fill=MID) label(d, 40, 940, '01', 'PHOTO', WH) save(im, 299) # ---------- Log#3 LOG300:同じ青い面の色相が、光の色だけで 206° → 51°(2枚を並べる) def log300(): reg = json.load(open(V + 'Log03/measure_regions.json'))['regions']['blue_front_left'] im = Image.new('RGB', (W, H), BK); d = ImageDraw.Draw(im) for i, (fn, hue, nm) in enumerate([('IMG_7913.JPG', '206°', 'COOL LIGHT'), ('IMG_7914.JPG', '51°', 'WARM LIGHT')]): src = load(V + 'Log03/' + fn); px, py = int(reg['nx'] * src.shape[1]), int(reg['ny'] * src.shape[0]) cw, ch = 1100, 1000 cr, (ox, oy) = cropc(src, px + 60, py - 220, cw, ch) ph = fit(cr, 940, 855); im.paste(Image.fromarray(ph), (i * 980, 0)) k = 940 / cw; qx, qy = i * 980 + (px - ox) * k, (py - oy) * k; q = 100 * k / 2 + 6 d.rectangle([qx - q, qy - q, qx + q, qy + q], outline=WH, width=5) d.text((i * 980 + 40, 880), hue, font=F(IB, 120), fill=BLUE if i == 0 else WH) d.text((i * 980 + 40 + d.textlength(hue, font=F(IB, 120)) + 30, 950), nm, font=F(MONO, 34), fill=WH) d.text((i * 980 + 40, 1030), 'HUE OF THE SAME BLUE FACE', font=F(MONO, 22), fill=MID) d.text((860, 905), '→', font=F(IB, 90), fill=WH) save(im, 300) # ---------- Log#4 LOG301:オレンジの光で、RGB の赤検出が画面の96%を拾う(大きなマスク + 4条件の帯) def log301(): t = json.load(open(U + 'vision/measurements/Log04/thresholds_white.json'))['thresholds'] conds = [('IMG_7913.JPG', 'BASE'), ('IMG_7911.JPG', 'DIM'), ('IMG_7912.JPG', 'BRIGHT'), ('IMG_7914.JPG', 'ORANGE')] rates = {}; masks = {}; photos = {} for fn, nm in conds: a = load(V + 'Log03/' + fn).astype(int) m = ((a[..., 0] - a[..., 1] > t['t_rg']) & (a[..., 0] - a[..., 2] > t['t_rb'])) rates[nm] = m.mean(); masks[nm] = m; photos[nm] = a.astype(np.uint8) print('301 rates', rates) im = Image.new('RGB', (W, H), BK); d = ImageDraw.Draw(im) mk = masks['ORANGE'].astype(np.uint8) * 255 big = fit(np.stack([mk] * 3, -1), 1360, 1020); big = np.where(big > 127, np.array(WH), np.array(BK)).astype(np.uint8) im.paste(Image.fromarray(big), (30, 30)) d.text((80, 80), f'{rates["ORANGE"] * 100:.1f}%', font=F(IB, 160), fill=BK) d.text((86, 280), 'OF THE FRAME READ AS RED', font=F(MONO, 34), fill=BK) d.text((86, 960), 'RGB RULE · TUNED UNDER WHITE LIGHT', font=F(MONO, 26), fill=BK) x, tw, th = 1430, 460, 230 for i, (fn, nm) in enumerate(conds): y = 30 + i * (th + 33) im.paste(Image.fromarray(fit(photos[nm], tw, int(tw * 3024 / 4032))[:th]), (x, y)) hi = nm == 'ORANGE' if hi: d.rectangle([x - 6, y - 6, x + tw + 5, y + th + 5], outline=ORANGE, width=6) d.rectangle([x, y + th - 46, x + tw, y + th], fill=BK) d.text((x + 14, y + th - 40), f'{nm:<7}{rates[nm] * 100:5.1f}%', font=F(MONO, 30), fill=ORANGE if hi else WH) save(im, 301) # ---------- Log#5 LOG302:モルフォロジー(上に写真の帯、下に3枚と 36 → 1) def log302(): src = load(V + 'Log05/mat_white_ref_20260729_211605_f0.png') m = biggest(redmask(src)); ys, xs = np.nonzero(m); cx, cy = int(xs.mean()), int(ys.mean()) im = Image.new('RGB', (W, H), BK); d = ImageDraw.Draw(im) band = fit(src[cy - 170:cy + 170, cx - 420:cx + 540], 1920, 680, cv2.INTER_LANCZOS4); g = gray(band).astype(float) rm = cv2.morphologyEx(redmask(band, 80, 50) * 255, cv2.MORPH_CLOSE, np.ones((25, 25), np.uint8)) rm = cv2.GaussianBlur(rm, (0, 0), 2)[..., None] / 255. band = (band * rm + g * (1 - rm)).astype(np.uint8) im.paste(Image.fromarray(band[170:600]), (0, 0)) label(d, 40, 330, '01', 'PHOTO', BK) crop = src[cy - 90:cy + 90, cx - 150:cx + 150] raw = redmask(crop, 60, 60); op = cv2.morphologyEx(raw, cv2.MORPH_OPEN, np.ones((5, 5), np.uint8)) cl = cv2.morphologyEx(op, cv2.MORPH_CLOSE, np.ones((15, 15), np.uint8)) nr = cv2.connectedComponents(raw)[0] - 1; no = cv2.connectedComponents(op)[0] - 1 pw, phh, gap, x, y = 560, 340, 40, 40, 600 for i, (mm, nm, num) in enumerate([(raw, 'THRESHOLD', '02'), (op, 'OPENING', '03'), (cl, 'CLOSING', '04')]): p = fit(mm * 255, pw, phh, cv2.INTER_NEAREST) im.paste(Image.fromarray(np.where(p[..., None] > 0, np.array(WH), np.array(DK)).astype(np.uint8)), (x + i * (pw + gap), y)) d.text((x + i * (pw + gap), y + phh + 24), num + ' ' + nm, font=F(MONO, 34), fill=WH) s = f'{nr} → {no}' d.text((40, 462), s, font=F(IB, 96), fill=WH) d.text((40 + d.textlength(s, font=F(IB, 96)) + 28, 500), 'BLOBS AFTER OPENING', font=F(MONO, 34), fill=WH) save(im, 302); print('302', nr, no) # ---------- Log#6 LOG304:円形度(左に写真、右に輪郭と数字) def log304(): src = load(V + 'Log06/shapes_red_rot30_20260731_190825_f0.png') r = redmask(src, 90, 60); n, l, s, ce = cv2.connectedComponentsWithStats(r); i = 1 + np.argmax(s[1:, 4]); cx, cy = [int(v) for v in ce[i]] big = (l == i).astype(np.uint8) h0 = 640; w0 = int(h0 * 960 / 1080); x0 = cx - w0 // 2 - 120; y0 = cy - h0 // 2 - 140 ph = src[y0:y0 + h0, x0:x0 + w0]; bm = cv2.dilate(big[y0:y0 + h0, x0:x0 + w0], np.ones((5, 5), np.uint8))[..., None] ph = fit((ph * bm + gray(ph) * (1 - bm)).astype(np.uint8), 960, 1080, cv2.INTER_LANCZOS4) im = Image.new('RGB', (W, H), WH); im.paste(Image.fromarray(ph), (0, 0)); d = ImageDraw.Draw(im) label(d, 40, 940, '01', 'PHOTO', BK) c0 = max(cv2.findContours(big * 255, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0], key=cv2.contourArea) circ = 4 * np.pi * cv2.contourArea(c0) / cv2.arcLength(c0, True) ** 2 bx, by, bw, bh = cv2.boundingRect(c0); k = 440 / max(bw, bh); ox, oy = 1440 - bw * k / 2, 470 - bh * k / 2 pts = [(ox + (p[0][0] - bx) * k, oy + (p[0][1] - by) * k) for p in c0] d.line(pts + [pts[0]], fill=BK, width=6) dashed(d, (ox - 20, oy - 20, ox + bw * k + 20, oy + bh * k + 20), MID) M = cv2.moments(c0); mx = ox + (M['m10'] / M['m00'] - bx) * k; my = oy + (M['m01'] / M['m00'] - by) * k d.line([(mx - 22, my), (mx + 22, my)], fill=RED, width=5); d.line([(mx, my - 22), (mx, my + 22)], fill=RED, width=5) d.text((1060, 800), f'{circ:.3f}', font=F(IB, 150), fill=BK); d.text((1066, 980), 'CIRCULARITY (1.000 = CIRCLE)', font=F(MONO, 30), fill=BK) save(im, 304); print('304', circ) # ---------- Log#7 LOG309:本の写真と、本のないマットの写真が「同じ」と結ばれる(2枚 + 対応の線) def log309(): a = load(V + 'Log07/flat_0_20260802_072613/f0.png'); b = load(V + 'Log07/mat_0_20260802_080524/f0.png') ca, _ = cropc(a, 860, 560, 700, 760); cb, _ = cropc(b, 860, 560, 700, 760) orb = cv2.ORB_create(3000) ka, da = orb.detectAndCompute(cv2.cvtColor(ca, cv2.COLOR_RGB2GRAY), None); kb, db = orb.detectAndCompute(cv2.cvtColor(cb, cv2.COLOR_RGB2GRAY), None) ms = sorted(cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True).match(da, db), key=lambda m: m.distance) pw, ph = 880, 955; k = pw / 700 im = Image.new('RGB', (W, H), BK); d = ImageDraw.Draw(im) im.paste(Image.fromarray(fit(gray(ca), pw, ph)), (40, 40)); im.paste(Image.fromarray(fit(gray(cb), pw, ph)), (1000, 40)) rng = np.random.default_rng(0); sel = [ms[i] for i in sorted(rng.choice(min(len(ms), 400), 70, replace=False))] for m in sel: p = ka[m.queryIdx].pt; q = kb[m.trainIdx].pt p = (40 + p[0] * k, 40 + p[1] * k); q = (1000 + q[0] * k, 40 + q[1] * k) d.line([p, q], fill=GREEN, width=2); d.ellipse([p[0] - 5, p[1] - 5, p[0] + 5, p[1] + 5], fill=GREEN); d.ellipse([q[0] - 5, q[1] - 5, q[0] + 5, q[1] + 5], fill=GREEN) d.rectangle([1000, 40 + ph - 210, 1000 + pw, 40 + ph], fill=BK) d.text((1030, 40 + ph - 200), '867 / 934', font=F(IB, 110), fill=GREEN) d.text((1036, 40 + ph - 60), 'INLIERS · NOTHING IN COMMON', font=F(MONO, 30), fill=WH) for x, t in [(40, 'BOOK'), (1000, 'EMPTY MAT')]: d.rectangle([x, 40, x + 20 + d.textlength(t, font=F(MONO, 34)) + 20, 100], fill=BK); d.text((x + 20, 50), t, font=F(MONO, 34), fill=WH) save(im, 309); print('309 orb matches', len(ms)) # ---------- Log#8 LOG310:斜めの紙を真上から見た形に(写真の4点 → 直した形) def log310(): src = load(V + 'Log08/tilt_30_20260807_082518/f0.png') g = cv2.cvtColor(src, cv2.COLOR_RGB2GRAY); _, wm = cv2.threshold(g, 200, 255, cv2.THRESH_BINARY) wm[:, :500] = 0; wm[:, 1500:] = 0 # マットの外の白い机を除く wm = cv2.morphologyEx(wm, cv2.MORPH_OPEN, np.ones((9, 9), np.uint8)) c = max(cv2.findContours(wm, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0], key=cv2.contourArea) ap = cv2.approxPolyDP(c, 0.02 * cv2.arcLength(c, True), True).reshape(-1, 2).astype(np.float32) if len(ap) != 4: ap = cv2.boxPoints(cv2.minAreaRect(c)).astype(np.float32) s_ = ap.sum(1); df = np.diff(ap, axis=1).ravel() q = np.array([ap[np.argmin(s_)], ap[np.argmin(df)], ap[np.argmax(s_)], ap[np.argmax(df)]], np.float32) # 左上・右上・右下・左下 wv = max(np.linalg.norm(q[0] - q[1]), np.linalg.norm(q[3] - q[2])); hv = max(np.linalg.norm(q[0] - q[3]), np.linalg.norm(q[1] - q[2])) landscape = wv > hv; ow, oh = (900, int(900 * 210 / 297)) if landscape else (int(900 * 210 / 297), 900) Hm = cv2.getPerspectiveTransform(q, np.array([[0, 0], [ow, 0], [ow, oh], [0, oh]], np.float32)) flat = cv2.warpPerspective(src, Hm, (ow, oh)) im = Image.new('RGB', (W, H), WH); d = ImageDraw.Draw(im) cr, (ox, oy) = cropc(src, q[:, 0].mean(), q[:, 1].mean(), 1000, 1080 * 1000 // 1000) k = 1000 / cr.shape[1]; phc = fit(gray(cr), 1000, int(cr.shape[0] * k))[:1080] im.paste(Image.fromarray(phc), (0, 0)) pts = [((p[0] - ox) * k, (p[1] - oy) * k) for p in q] d.line(pts + [pts[0]], fill=CYAN, width=4) for p in pts: d.ellipse([p[0] - 14, p[1] - 14, p[0] + 14, p[1] + 14], fill=CYAN, outline=BK, width=3) d.rectangle([0, 920, 420, 1080], fill=WH); label(d, 40, 940, '01', 'TILTED PLANE', BK) fw = 760; fh = int(oh * fw / ow); fl = fit(gray(flat), fw, fh) fx, fy = 1100, (1080 - fh) // 2 - 60 im.paste(Image.fromarray(fl), (fx, fy)) d.rectangle([fx, fy, fx + fw, fy + fh], outline=BK, width=3) d.text((1100, fy + fh + 40), '02', font=F(MONO, 30), fill=BK); d.text((1100, fy + fh + 82), 'FRONT-PARALLEL · 4 POINTS', font=F(MONO, 38), fill=BK) d.text((1020, 470), '→', font=F(IB, 70), fill=BK) save(im, 310) # ---------- Log#9 LOG313:用語の5段(上に段の表、下に写真の帯) def log313(): src = load(V + 'Log05/mat_white_ref_20260729_211605_f0.png') im = Image.new('RGB', (W, H), WH); d = ImageDraw.Draw(im) band = gray(fit(src[545:745, 380:1540], 1920, 331)) im.paste(Image.fromarray(band[:330]), (0, 750)) rows = [('4', 'TOOLS', 'ROBOFLOW · HUGGING FACE'), ('3', 'FIELD', 'OCCLUSION · POINT CLOUD · NIR'), ('2', 'MODELS', 'DINOv3 · YOLO26 · SAM 3 · DEPTH ANYTHING'), ('1', 'OUTPUTS', 'DETECT · SEGMENT · DEPTH'), ('0', 'BASICS', 'MODEL · PARAMETER · LOSS · BACKBONE')] y = 50 for num, h, ws in rows: hi = num == '0' if hi: d.rectangle([40, y - 10, 1880, y + 110], fill=BK) col = WH if hi else BK d.text((70, y + 18), num, font=F(IB, 64), fill=AMBER if hi else col) d.text((170, y + 30), h, font=F(MONO, 40), fill=col) d.text((520, y + 30), ws, font=F(MONO, 40), fill=col) y += 136 d.text((40, 712), 'FIVE LAYERS OF WORDS · NO CODE THIS TIME', font=F(MONO, 28), fill=MID) save(im, 313) # ---------- Log#10 LOG317:ガラス瓶とペットボトルが同じ4本(生成画像に4つの枠) def log317(): src = load(U + 'vision/generated/Log10/B_tray_similar.png') # 記事で使った生成画像 k = 1080 / src.shape[0]; w = int(src.shape[1] * k) im = Image.new('RGB', (W, H), BK); im.paste(Image.fromarray(fit(gray(src), w, 1080)), (W - w, 0)); d = ImageDraw.Draw(im) ox = W - w boxes = [(440, 100, 690, 530), (975, 115, 1345, 552), (140, 478, 472, 978), (712, 402, 1028, 975)] for x0, y0, x1, y1 in boxes: d.rectangle([ox + x0 * k, y0 * k, ox + x1 * k, y1 * k], outline=TEAL, width=6) lx = 50 d.text((lx, 90), '"glass"', font=F(MONO, 52), fill=WH); d.text((lx, 170), '"plastic"', font=F(MONO, 52), fill=WH) d.text((lx, 330), '= same 4', font=F(IB, 82), fill=TEAL) d.text((lx, 960), 'SAM 3 · TEXT PROMPT', font=F(MONO, 30), fill=MID); d.text((lx, 1000), 'GENERATED IMAGE', font=F(MONO, 26), fill=MID) save(im, 317) # ---------- Log#11 LOG325:持ち上げる前と後が、真上からは同じに見える(2コマ) def log325(): cap = cv2.VideoCapture(U + 'runs/run_pick_place/run_0001_20261019_195120/recording/top.mp4') def fr(i): cap.set(1, i); ok, f = cap.read(); return cv2.cvtColor(f, cv2.COLOR_BGR2RGB) a, b = fr(440), fr(560) im = Image.new('RGB', (W, H), WH); d = ImageDraw.Draw(im) for i, (f, nm) in enumerate([(a, 'BEFORE LIFT'), (b, 'AFTER LIFT')]): r = redmask(f, 110, 60); ys, xs = np.nonzero(biggest(r)); cx, cy = xs.mean(), ys.mean() cr, _ = cropc(f, cx, cy - 60, 420, 470) g = gray(cr).astype(float); rm = cv2.GaussianBlur(cv2.dilate(redmask(cr, 110, 60) * 255, np.ones((5, 5), np.uint8)), (0, 0), 1.5)[..., None] / 255. cr = (cr * rm + g * (1 - rm)).astype(np.uint8) im.paste(Image.fromarray(fit(cr, 900, 1007)), (40 + i * 940, 0)) d.rectangle([40 + i * 940, 880, 40 + i * 940 + 900, 1007], fill=BK) d.text((80 + i * 940, 905), f'0{i + 1}', font=F(MONO, 30), fill=WH); d.text((80 + i * 940, 945), nm, font=F(MONO, 38), fill=WH) d.text((40, 1025), 'TOP CAMERA · HEIGHT IS NOT IN THE PICTURE', font=F(MONO, 30), fill=BK) d.text((1540, 900), '= ?', font=F(IB, 100), fill=WH) save(im, 325) # ---------- Log#12 LOG459:1枚の写真と6つの答え(大きい写真 + 6枚の格子) def log459(): src = load(V + 'Log05/mat_orange_20260729_212044_f0.png') cr, _ = cropc(src, 960, 500, 700, 1000) im = Image.new('RGB', (W, H), BK); im.paste(Image.fromarray(fit(cr, 756, 1080)), (0, 0)); d = ImageDraw.Draw(im) label(d, 40, 940, '00', 'PHOTO', WH) tiles = ['cls', 'det', 'seg', 'pose', 'trk', 'dep']; names = ['CLASSIFY', 'DETECT', 'SEGMENT', 'POSE', 'TRACK', 'DEPTH'] tw, th = 368, 245 for i, (t, nm) in enumerate(zip(tiles, names)): a = np.array(Image.open(f'{SITE}/public/site/vision/{t}.webp').convert('RGB')) x = 790 + (i % 3) * (tw + 12); y = 30 + (i // 3) * (th + 120) if i >= 3: y += 0 im.paste(Image.fromarray(fit(a, tw, th)), (x, y)) d.text((x, y + th + 18), f'0{i + 1} {nm}', font=F(MONO, 30), fill=AMBER if i == 0 else WH) d.text((790, 800), 'ONE PHOTO', font=F(IB, 96), fill=WH); d.text((790, 910), 'SIX ANSWERS', font=F(IB, 96), fill=AMBER) save(im, 459) # ---------- LOG236:SAM 3 が、ステンレスのトレイに映った像までマスクした(大きな1コマ + 右に言葉) def log236(): # 記事の動画(R2 の media/s-1280x720_0e7779e9-0f3a-45fd-aa67-b76c819faabb.mp4。SAM 3 のマスクが焼き込まれた画面)の 110 コマ目 cap = cv2.VideoCapture(os.environ.get('LOG236_MP4', '/tmp/claude-0/s/l236/s-1280x720_0e7779e9-0f3a-45fd-aa67-b76c819faabb.mp4')) cap.set(1, 110); ok, f = cap.read(); f = cv2.cvtColor(f, cv2.COLOR_BGR2RGB) hsv = cv2.cvtColor(f, cv2.COLOR_RGB2HSV) blue = ((hsv[..., 0] > 85) & (hsv[..., 0] < 125) & (hsv[..., 1] > 70) & (hsv[..., 2] > 150)).astype(np.uint8) blue[:540, :] = 0; blue[:, :650] = 0; blue[:, 990:] = 0 # 映り込みに付いたマスクだけ blue = cv2.dilate(blue, np.ones((3, 3), np.uint8)); m = cv2.GaussianBlur(blue.astype(float), (0, 0), 1)[..., None] f = (f * m + gray(f) * (1 - m)).astype(np.uint8) x0 = 250; cr = f[:, x0:x0 + 867] # 右下の飾り(✦)は外す im = Image.new('RGB', (W, H), BK); im.paste(Image.fromarray(fit(cr, 1300, 1080)), (0, 0)); d = ImageDraw.Draw(im) ys, xs = np.nonzero(blue); k = 1300 / 867 bx0, by0, bx1, by1 = (xs.min() - x0) * k - 24, ys.min() * k - 24, (xs.max() - x0) * k + 24, ys.max() * k + 24 dashed(d, (bx0, by0, bx1, by1), WH, width=4) d.rectangle([bx0, by0 - 52, bx0 + 270, by0], fill=WH); d.text((bx0 + 14, by0 - 46), 'REFLECTION', font=F(MONO, 32), fill=BK) x = 1370 d.text((x, 90), 'SAM 3 · STEEL TRAY', font=F(MONO, 30), fill=MID) d.text((x, 300), 'MASKED', font=F(IB, 96), fill=WH) d.text((x, 410), 'A MIRROR', font=F(IB, 96), fill=BLUE) d.text((x, 520), 'IMAGE.', font=F(IB, 96), fill=WH) d.text((x, 900), 'SHAPE ≠ SOMETHING', font=F(MONO, 30), fill=WH); d.text((x, 945), 'TO GRASP', font=F(MONO, 30), fill=WH) save(im, 236) if __name__ == '__main__': import sys fns = {k[3:]: v for k, v in globals().items() if k.startswith('log') and callable(v)} for k in (sys.argv[1:] or sorted(fns)): fns[k]()