# 第133話 英語版:吹き出しの日本語を消して英語をフォントで組む(2026-10-31 試作) import json, numpy as np from collections import deque from PIL import Image, ImageDraw, ImageFont SRC="../仕上げ/第133話_P{}_1080x1350.png"; OUT="第133話_P{}_EN_1080x1350.png" import os FONTS=os.path.expanduser("~/mnt/Fonts") # Mac の ~/Library/Fonts(Comic Neue。今までの英語版マンガに合わせる) F_HUMAN=FONTS+"/ComicNeue-Bold.ttf" F_ROBOT=F_HUMAN F_BOARD=FONTS+"/ComicNeue-Bold.ttf" # 吹き出し:(コマ, 吹き出しの中の1点[x,y], 英語, フォント) BUBBLES=[ (1,(550,166),"The robot is all set and ready to go!",F_HUMAN), (3,(540,213),"I am ready to roll.\nAny time.",F_ROBOT), (4,(310,177),"So before we moved\nthe robot, we had to\nmove everyone.",F_HUMAN), (4,(809,177),"For the next ward,\nall six sign-offs go out\non day one.",F_HUMAN), (4,(545,411),"Heading out.",F_ROBOT), ] LABELS=["Pharmacy","Nursing","Facilities","IT","Fire\nSafety","Patient\nSafety"] # ホワイトボード:縦線のx(左の枠〜右の枠)、札の帯のy(上・下) BOARDS={1:dict(xs=[101,221,324,427,530,633,771],y=(330,389)), 2:dict(xs=[77,238,393,548,704,858,1017],y=(266,333))} def fill_region(g, seed, thr=230): H,W=g.shape; m=np.zeros((H,W),bool); q=deque([seed]); m[seed[1],seed[0]]=True while q: x,y=q.popleft() for dx,dy in ((1,0),(-1,0),(0,1),(0,-1)): xx,yy=x+dx,y+dy if 0<=xxthr: m[yy,xx]=True; q.append((xx,yy)) return m def interior(comp): ys,xs=np.nonzero(comp); y0,y1,x0,x1=ys.min(),ys.max(),xs.min(),xs.max() sub=comp[y0:y1+1,x0:x1+1]; h,w=sub.shape out=np.zeros((h+2,w+2),bool); pad=np.zeros((h+2,w+2),bool); pad[1:-1,1:-1]=sub q=deque([(0,0)]); out[0,0]=True while q: x,y=q.popleft() for dx,dy in ((1,0),(-1,0),(0,1),(0,-1)): xx,yy=x+dx,y+dy if 0<=xx=0.6*widths.max()] cy=(core[0]+core[-1])/2; cx=np.mean([ (spans[y][0]+spans[y][1])/2 for y in core]) for size in range(smax,15,-1): font=ImageFont.truetype(fpath,size); lh=int(size*1.18) for maxw in range(int(widths.max()),60,-10): lines=wrap(draw,text,font,maxw); H=lh*len(lines); top=cy-H/2 ok=True; place=[] for i,l in enumerate(lines): lw=draw.textlength(l,font=font); a=int(top+i*lh); b=int(top+i*lh+lh) x=cx-lw/2 for y in range(a-pad//2,b+pad//2): if y not in spans or spans[y][0]+pad>x or spans[y][1]-pad245: c=fill_region(g,(x,y)) if c.sum()>5000: comp=c;break if comp is not None: break print(p,seed,int(comp.sum())) assert not (comp[0].any() or comp[-1].any() or comp[:,0].any() or comp[:,-1].any()), "吹き出しが閉じていない" ins=interior(comp) # 文字を消す範囲:内側から縁へ6px離す(縁の線を削らない) er=ins.copy() for _ in range(6): e=er.copy(); e[1:]&=er[:-1]; e[:-1]&=er[1:]; e[:,1:]&=er[:,:-1]; e[:,:-1]&=er[:,1:]; er=e left=(g<200)&ins&~er white=np.median(a[comp],axis=0).astype(np.uint8) a[er]=white; allowed|=ins texts.append((text,fp,ins,int(left.sum()))) if p in BOARDS: bd=BOARDS[p]; y0,y1=bd["y"]; boxes=[] for i,lab in enumerate(LABELS): xa,xb=bd["xs"][i]+5,bd["xs"][i+1]-4 dk=g[y0:y1,xa:xb]<200; cols=np.nonzero(dk.sum(0))[0] cl=[];st=cols[0];pv=cols[0] for c in cols[1:]: if c-pv>10: cl.append((st,pv)); st=c pv=c cl.append((st,pv)) # セルの端に触れているかたまりは札ではない(人物・枠)。消さない keep=[c for c in cl if c[0]>3 and c[1]<(xb-xa)-4] or cl lx0,lx1=keep[0][0],keep[-1][1] rows=np.nonzero(dk[:,lx0:lx1+1].sum(1))[0] bx0=max(xa,xa+lx0-4); bx1=min(xb,xa+lx1+5); by0=max(y0,y0+rows[0]-4); by1=min(y1,y0+rows[-1]+5) cell=np.zeros(g.shape,bool); cell[by0:by1,bx0:bx1]=True a[cell]=(254,254,254); allowed|=cell # 英語を置ける幅:消さなかったかたまりの手前まで right=xb if keep[-1]==cl[-1] else xa+cl[cl.index(keep[-1])+1][0]-4 left=xa if keep[0]==cl[0] else xa+cl[cl.index(keep[0])-1][1]+4 area=np.zeros(g.shape,bool); area[y0+3:y1-3,left:right]=True stray=int(((g<200)&area&~cell).sum()) print("P",p,lab,"札の外の暗い画素",stray) allowed|=area; boxes.append((lab,area)) # 6つの札は同じ大きさにそろえる d0=ImageDraw.Draw(Image.new("RGB",(10,10))) def best(lab,area): ys,xs=np.nonzero(area); w=xs.max()-xs.min(); h=ys.max()-ys.min() for size in range(44 if p==2 else 30,8,-1): f=ImageFont.truetype(F_BOARD,size); lines=wrap(d0,lab,f,w-8); lh=int(size*1.08) if max(d0.textlength(l,font=f) for l in lines)<=w-8 and lh*len(lines)<=h: return size S=min(best(l,ar) for l,ar in boxes) for lab,area in boxes: texts.append((lab,F_BOARD,area,-S)) img=Image.fromarray(a); d=ImageDraw.Draw(img); info=[] for text,fp,ins,left in texts: if left<0: size=-left; ys,xs=np.nonzero(ins); w=xs.max()-xs.min() f=ImageFont.truetype(fp,size); lines=wrap(d,text,f,w-8); lh=int(size*1.08) top=(ys.min()+ys.max())/2-lh*len(lines)/2-size*0.08 for k,l in enumerate(lines): d.text(((xs.min()+xs.max())/2-d.textlength(l,font=f)/2,top+k*lh),l,font=f,fill=(25,25,25)) info.append(dict(text=text,size=size,lines=lines)) else: smax=44 f,place,lh=fit(d,text,fp,ins,smax) for x,y,l in place: d.text((x,y),l,font=f,fill=(15,15,15)) info.append(dict(text=text,size=f.size,lines=[l for _,_,l in place],縁に残った暗い画素=left)) out=np.asarray(img); changed=(np.abs(out.astype(int)-np.asarray(im).astype(int)).sum(2)>0) report[p]=dict(items=info,吹き出しの外で変わった画素=int((changed&~allowed).sum())) img.save(OUT.format(p)) json.dump(report,open("確認_数値.json","w"),ensure_ascii=False,indent=1) print(json.dumps(report,ensure_ascii=False,indent=1))